diff --git a/.circleci/config.yml b/.circleci/config.yml index f102a5659..054a60a28 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -9,20 +9,20 @@ version: 2.1 parameters: ubuntu-2004-docker-image: type: string - # solbuildpackpusher/solidity-buildpack-deps:ubuntu2004-5 - default: "solbuildpackpusher/solidity-buildpack-deps@sha256:2d306b8da3485c2584a8868d656dc36c1ae50f003ff085ad2e904e312534b9b7" + # solbuildpackpusher/solidity-buildpack-deps:ubuntu2004-6 + default: "solbuildpackpusher/solidity-buildpack-deps@sha256:da44d7f78e093f7f0415abf07f7c1fd1c2ed4fa65fefea428821a05186c42ec9" ubuntu-2004-clang-docker-image: type: string - # solbuildpackpusher/solidity-buildpack-deps:ubuntu2004.clang-5 - default: "solbuildpackpusher/solidity-buildpack-deps@sha256:4fbc7a99dd0b204fef587856d89640e4b2060d459ba15c32b89733b2a6054d7f" + # solbuildpackpusher/solidity-buildpack-deps:ubuntu2004.clang-6 + default: "solbuildpackpusher/solidity-buildpack-deps@sha256:c78dd9c48d393b57afe053aeb2d0d358a9f31ac85039a181724c2f8408d0bcf8" ubuntu-1604-clang-ossfuzz-docker-image: type: string - # solbuildpackpusher/solidity-buildpack-deps:ubuntu1604.clang.ossfuzz-8 - default: "solbuildpackpusher/solidity-buildpack-deps@sha256:42f47b7ddafbf57b4e48357022cf34dc38ae477b05ddc2210e7ed68d821c2019" + # solbuildpackpusher/solidity-buildpack-deps:ubuntu1604.clang.ossfuzz-9 + default: "solbuildpackpusher/solidity-buildpack-deps@sha256:5078e1d74ab6f4329e9218c2d8c0ebe2d42817a3d4c3c62ce887100cbe9bc739" emscripten-docker-image: type: string - # solbuildpackpusher/solidity-buildpack-deps:emscripten-4 - default: "solbuildpackpusher/solidity-buildpack-deps@sha256:434719d8104cab47712dd1f56f255994d04eb65b802c0d382790071c1a0c074b" + # solbuildpackpusher/solidity-buildpack-deps:emscripten-5 + default: "solbuildpackpusher/solidity-buildpack-deps@sha256:d28afb9624c2352ea40f157d1a321ffac77f54a21e33a8e8744f9126b780ded4" orbs: win: circleci/windows@2.2.0 @@ -354,8 +354,8 @@ jobs: command: apt -q update && apt install -y python3-pip - run: name: Install pylint - command: python3 -m pip install pylint z3-solver pygments-lexer-solidity - # also z3-solver to make sure pylint knows about this module, pygments-lexer-solidity for docs + command: python3 -m pip install pylint z3-solver pygments-lexer-solidity parsec tabulate + # also z3-solver, parsec and tabulate to make sure pylint knows about this module, pygments-lexer-solidity for docs - run: name: Linting Python Scripts command: ./scripts/pylint_all.py @@ -571,7 +571,9 @@ jobs: b_archlinux: docker: - - image: archlinux/base + # FIXME: Newer releases won't work until CircleCI updates its host machines. + # See https://github.com/ethereum/solidity/pull/11332 + - image: archlinux:base-20210131.0.14634 environment: TERM: xterm MAKEFLAGS: -j 3 @@ -710,11 +712,19 @@ jobs: t_archlinux_soltest: &t_archlinux_soltest docker: - - image: archlinux/base + # FIXME: Newer releases won't work until CircleCI updates its host machines. + # See https://github.com/ethereum/solidity/pull/11332 + - image: archlinux:base-20210131.0.14634 environment: EVM: constantinople OPTIMIZE: 0 TERM: xterm + # For Archlinux we do not have prebuilt docker images and we would need to build evmone from source, + # thus we forgo semantics tests to speed things up. + # FIXME: Z3 4.8.11 prerelease is now in main Arch Linux repos but it makes some of our SMT + # tests hang. Disabling SMT tests until we get a proper release. + # See https://github.com/Z3Prover/z3/issues/5330 for more details. + SOLTEST_FLAGS: --no-semantic-tests --no-smt steps: - run: name: Install runtime dependencies diff --git a/.circleci/osx_install_dependencies.sh b/.circleci/osx_install_dependencies.sh index 2845b224a..f1dd4f11b 100755 --- a/.circleci/osx_install_dependencies.sh +++ b/.circleci/osx_install_dependencies.sh @@ -37,6 +37,8 @@ set -eu if [ ! -f /usr/local/lib/libz3.a ] # if this file does not exists (cache was not restored), rebuild dependencies then + git -C /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core fetch --unshallow + git -C /usr/local/Homebrew/Library/Taps/homebrew/homebrew-cask fetch --unshallow brew update brew unlink python brew install boost @@ -55,12 +57,12 @@ then rm -rf z3-4.8.10-x64-osx-10.15.7 # evmone - wget https://github.com/ethereum/evmone/releases/download/v0.4.0/evmone-0.4.0-darwin-x86_64.tar.gz - tar xzpf evmone-0.4.0-darwin-x86_64.tar.gz -C /usr/local - rm -f evmone-0.4.0-darwin-x86_64.tar.gz + wget https://github.com/ethereum/evmone/releases/download/v0.7.0/evmone-0.7.0-darwin-x86_64.tar.gz + tar xzpf evmone-0.7.0-darwin-x86_64.tar.gz -C /usr/local + rm -f evmone-0.7.0-darwin-x86_64.tar.gz # hera - wget https://github.com/ewasm/hera/releases/download/v0.3.2/hera-0.3.2-darwin-x86_64.tar.gz - tar xzpf hera-0.3.2-darwin-x86_64.tar.gz -C /usr/local - rm -f hera-0.3.2-darwin-x86_64.tar.gz + wget https://github.com/ewasm/hera/releases/download/v0.3.2-evmc8/hera-0.3.2+commit.dc886eb7-darwin-x86_64.tar.gz + tar xzpf hera-0.3.2+commit.dc886eb7-darwin-x86_64.tar.gz -C /usr/local + rm -f hera-0.3.2+commit.dc886eb7-darwin-x86_64.tar.gz fi diff --git a/.circleci/soltest_all.sh b/.circleci/soltest_all.sh index c407ea627..a79228637 100755 --- a/.circleci/soltest_all.sh +++ b/.circleci/soltest_all.sh @@ -28,7 +28,9 @@ set -e REPODIR="$(realpath "$(dirname "$0")"/..)" -EVM_VALUES=(homestead byzantium constantinople petersburg istanbul) +EVM_VALUES=(homestead byzantium constantinople petersburg istanbul berlin) +DEFAULT_EVM=berlin +[[ " ${EVM_VALUES[*]} " =~ $DEFAULT_EVM ]] OPTIMIZE_VALUES=(0 1) STEPS=$(( 1 + ${#EVM_VALUES[@]} * ${#OPTIMIZE_VALUES[@]} )) @@ -45,7 +47,7 @@ STEP=1 # Run for ABI encoder v1, without SMTChecker tests. -[[ " $RUN_STEPS " == *" $STEP "* ]] && EVM=istanbul OPTIMIZE=1 ABI_ENCODER_V1=1 BOOST_TEST_ARGS="-t !smtCheckerTests" "${REPODIR}/.circleci/soltest.sh" +[[ " $RUN_STEPS " == *" $STEP "* ]] && EVM="${DEFAULT_EVM}" OPTIMIZE=1 ABI_ENCODER_V1=1 BOOST_TEST_ARGS="-t !smtCheckerTests" "${REPODIR}/.circleci/soltest.sh" STEP=$((STEP + 1)) for OPTIMIZE in "${OPTIMIZE_VALUES[@]}" @@ -56,7 +58,7 @@ do EWASM_ARGS="" [ "${EVM}" = "byzantium" ] && [ "${OPTIMIZE}" = "0" ] && EWASM_ARGS="--ewasm" ENFORCE_GAS_ARGS="" - [ "${EVM}" = "istanbul" ] && ENFORCE_GAS_ARGS="--enforce-gas-cost" + [ "${EVM}" = "${DEFAULT_EVM}" ] && ENFORCE_GAS_ARGS="--enforce-gas-cost" # Run SMTChecker tests only when OPTIMIZE == 0 DISABLE_SMTCHECKER="" [ "${OPTIMIZE}" != "0" ] && DISABLE_SMTCHECKER="-t !smtCheckerTests" diff --git a/.github/ISSUE_TEMPLATE/documentation_issue.md b/.github/ISSUE_TEMPLATE/documentation_issue.md new file mode 100644 index 000000000..c0706b5f3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation_issue.md @@ -0,0 +1,22 @@ +--- +name: Documentation Issue +about: Solidity documentation. +--- + +## Page + + + +## Abstract + + + +## Pull request + + diff --git a/.gitignore b/.gitignore index 0238cc349..9322b0c81 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ prerelease.txt /build* emscripten_build/ docs/_build +docs/_static/robots.txt __pycache__ docs/utils/*.pyc /deps/downloads/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 002396eeb..c35426f7d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,7 +21,7 @@ include(EthPolicy) eth_policy() # project name and version should be set after cmake_policy CMP0048 -set(PROJECT_VERSION "0.8.5") +set(PROJECT_VERSION "0.8.7") # OSX target needed in order to support std::visit set(CMAKE_OSX_DEPLOYMENT_TARGET "10.14") project(solidity VERSION ${PROJECT_VERSION} LANGUAGES C CXX) diff --git a/Changelog.md b/Changelog.md index 541d22e9e..2f10594ea 100644 --- a/Changelog.md +++ b/Changelog.md @@ -3,7 +3,78 @@ Breaking changes: * `error` is now a keyword that can only be used for defining errors. -### 0.8.5 (unreleased) + +### 0.8.7 (unreleased) + +Language Features: + + +Compiler Features: + * AssemblyStack: Also run opcode-based optimizer when compiling Yul code. + * Yul EVM Code Transform: Do not reuse stack slots that immediately become unreachable. + * Yul EVM Code Transform: Also pop unused argument slots for functions without return variables (under the same restrictions as for functions with return variables). + * Yul Optimizer: Move function arguments and return variables to memory with the experimental Stack Limit Evader (which is not enabled by default). + + +Bugfixes: + * Code Generator: Fix crash when passing an empty string literal to ``bytes.concat()``. + * Code Generator: Fix internal compiler error when calling functions bound to calldata structs and arrays. + * Code Generator: Fix internal compiler error when passing a 32-byte hex literal or a zero literal to ``bytes.concat()`` by disallowing such literals. + * Type Checker: Fix internal error and prevent static calls to unimplemented modifiers. + * Yul Code Generator: Fix internal compiler error when using a long literal with bitwise negation. + + +### 0.8.6 (2021-06-22) + +Language Features: + * Yul: Special meaning of ``".metadata"`` data object in Yul object. + + +Bugfixes: + * Control Flow Graph: Fix incorrectly reported unreachable code. + * Solc-Js: When running ``solcjs`` without the ``--optimize`` flag, use ``settings.optimizer.enabled=false`` in Standard JSON instead of omitting the key. + * Standard JSON: Omitting ``settings.optimizer.enabled`` was not equivalent to setting it to ``false``. It meant disabling also the peephole optimizer and jumpdest remover which by default still run with ``enabled=false``. + + +### 0.8.5 (2021-06-10) + +Language Features: + * Allowing conversion from ``bytes`` and ``bytes`` slices to ``bytes1``/.../``bytes32``. + * Yul: Add ``verbatim`` builtin function to inject arbitrary bytecode. + + +Compiler Features: + * Code Generator: Insert helper functions for panic codes instead of inlining unconditionally. This can reduce costs if many panics (checks) are inserted, but can increase costs where few panics are used. + * EVM: Set the default EVM version to "Berlin". + * SMTChecker: Function definitions can be annotated with the custom Natspec tag ``custom:smtchecker abstract-function-nondet`` to be abstracted by a nondeterministic value when called. + * Standard JSON / combined JSON: New artifact "functionDebugData" that contains bytecode offsets of entry points of functions and potentially more information in the future. + * Yul Optimizer: Evaluate ``keccak256(a, c)``, when the value at memory location ``a`` is known at compile time and ``c`` is a constant ``<= 32``. + + +Bugfixes: + * AST: Do not output value of Yul literal if it is not a valid UTF-8 string. + * Code Generator: Fix internal error when function arrays are assigned to storage variables and the function types can be implicitly converted but are not identical. + * Code Generator: Fix internal error when super would have to skip an unimplemented function in the virtual resolution order. + * Control Flow Graph: Assume unimplemented modifiers use a placeholder. + * Control Flow Graph: Take internal calls to functions that always revert into account for reporting unused or unassigned variables. + * Function Call Graph: Fix internal error connected with circular constant references. + * Name Resolver: Do not issue shadowing warning if the shadowing name is not directly accessible. + * Natspec: Allow multiple ``@return`` tags on public state variable documentation. + * SMTChecker: Fix internal error on conversion from ``bytes`` to ``fixed bytes``. + * SMTChecker: Fix internal error on external calls from the constructor. + * SMTChecker: Fix internal error on struct constructor with fixed bytes member initialized with string literal. + * Source Locations: Properly set source location of scoped blocks. + * Standard JSON: Properly allow the ``inliner`` setting under ``settings.optimizer.details``. + * Type Checker: Fix internal compiler error related to having mapping types in constructor parameter for abstract contracts. + * Type Checker: Fix internal compiler error when attempting to use an invalid external function type on pre-byzantium EVMs. + * Type Checker: Fix internal compiler error when overriding receive ether function with one having different parameters during inheritance. + * Type Checker: Make errors about (nested) mapping type in event or error parameter into fatal type errors. + * Type Checker: Fix internal compiler error when overriding an implemented modifier with an unimplemented one. + + +AST Changes: + * Add member `hexValue` for Yul string and hex literals. + ### 0.8.4 (2021-04-21) diff --git a/README.md b/README.md index 9a79a9cef..abc8616e0 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [![Gitter Chat](https://img.shields.io/badge/Gitter%20-chat-brightgreen?style=plastic&logo=gitter)](https://gitter.im/ethereum/solidity) [![Solidity Forum](https://img.shields.io/badge/Solidity_Forum%20-discuss-brightgreen?style=plastic&logo=discourse)](https://forum.soliditylang.org/) [![Twitter Follow](https://img.shields.io/twitter/follow/solidity_lang?style=plastic&logo=twitter)](https://twitter.com/solidity_lang) +[![Mastodon Follow](https://img.shields.io/mastodon/follow/000335908?domain=https%3A%2F%2Ffosstodon.org%2F&logo=mastodon&style=plastic)](https://fosstodon.org/@solidity) You can talk to us on Gitter and Matrix, tweet at us on Twitter or create a new topic in the Solidity forum. Questions, feedback, and suggestions are welcome! diff --git a/ReleaseChecklist.md b/ReleaseChecklist.md index af3bbfdaa..7fe7c361c 100644 --- a/ReleaseChecklist.md +++ b/ReleaseChecklist.md @@ -61,7 +61,6 @@ ### Documentation - [ ] Build the new version on https://readthedocs.org/projects/solidity/ (select `latest` at the bottom of the page and click `BUILD`). - [ ] In the admin panel, select `Versions` in the menu and set the default version to the released one. - - [ ] If it is a non-breaking release, block indexing of previous release version in the ``robots.txt`` file. ### Release solc-js - [ ] Wait until solc-bin was properly deployed. You can test this via remix - a test run through remix is advisable anyway. @@ -72,5 +71,7 @@ ### Post-release - [ ] Publish the blog post. - [ ] Create a commit to increase the version number on ``develop`` in ``CMakeLists.txt`` and add a new skeleton changelog entry. - - [ ] Announce on Twitter and Reddit. + - [ ] Announce on Twitter, including links to the release and the blog post. + - [ ] Share announcement on Reddit and Solidity forum. + - [ ] Update the release information section on [soliditylang.org](https://github.com/ethereum/solidity-portal). - [ ] Lean back, wait for bug reports and repeat from step 1 :) diff --git a/docs/050-breaking-changes.rst b/docs/050-breaking-changes.rst index 57aa8f80a..3076044d3 100644 --- a/docs/050-breaking-changes.rst +++ b/docs/050-breaking-changes.rst @@ -290,7 +290,7 @@ It is still possible to interface with contracts written for Solidity versions p v0.5.0 (or the other way around) by defining interfaces for them. Consider you have the following pre-0.5.0 contract already deployed: -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.4.25; @@ -308,7 +308,7 @@ Consider you have the following pre-0.5.0 contract already deployed: This will no longer compile with Solidity v0.5.0. However, you can define a compatible interface for it: -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.5.0 <0.9.0; @@ -326,7 +326,7 @@ the function will work with ``staticcall``. Given the interface defined above, you can now easily use the already deployed pre-0.5.0 contract: -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.5.0 <0.9.0; @@ -347,7 +347,7 @@ Similarly, pre-0.5.0 libraries can be used by defining the functions of the libr supplying the address of the pre-0.5.0 library during linking (see :ref:`commandline-compiler` for how to use the commandline compiler for linking): -:: +.. code-block:: solidity // This will not compile after 0.6.0 // SPDX-License-Identifier: GPL-3.0 @@ -372,7 +372,7 @@ v0.5.0 with some of the changes listed in this section. Old version: -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.4.25; @@ -435,7 +435,7 @@ Old version: New version: -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.5.0; diff --git a/docs/_static/robots.txt b/docs/_static/robots.txt deleted file mode 100644 index c526f3786..000000000 --- a/docs/_static/robots.txt +++ /dev/null @@ -1,95 +0,0 @@ -Sitemap: http://docs.soliditylang.org/sitemap.xml - -# Prevent documentation for the development branches from showing up in search results. -Disallow: /en/develop/ -Disallow: /en/breaking/ - -# Prevent documentation for the older Solidity versions from showing up in search results. - -Disallow: /en/v0.1.2/ -Disallow: /en/v0.1.3/ -Disallow: /en/v0.1.4/ -Disallow: /en/v0.1.5/ -Disallow: /en/v0.1.6/ -Disallow: /en/v0.1.7/ -Disallow: /en/v0.2.0/ -Disallow: /en/v0.2.1/ -Disallow: /en/v0.2.2/ -Disallow: /en/v0.3.0/ -Disallow: /en/v0.3.1/ -Disallow: /en/v0.3.2/ -Disallow: /en/v0.3.3/ -Disallow: /en/v0.3.4/ -Disallow: /en/v0.3.5/ -Disallow: /en/v0.3.6/ -Disallow: /en/v0.4.0/ -Disallow: /en/v0.4.1/ -Disallow: /en/v0.4.2/ -Disallow: /en/v0.4.3/ -Disallow: /en/v0.4.4/ -Disallow: /en/v0.4.5/ -Disallow: /en/v0.4.6/ -Disallow: /en/v0.4.7/ -Disallow: /en/v0.4.8/ -Disallow: /en/v0.4.9/ -Disallow: /en/v0.4.10/ -Disallow: /en/v0.4.11/ -Disallow: /en/v0.4.12/ -Disallow: /en/v0.4.13/ -Disallow: /en/v0.4.14/ -Disallow: /en/v0.4.15/ -Disallow: /en/v0.4.16/ -Disallow: /en/v0.4.17/ -Disallow: /en/v0.4.18/ -Disallow: /en/v0.4.19/ -Disallow: /en/v0.4.20/ -Disallow: /en/v0.4.21/ -Disallow: /en/v0.4.22/ -Disallow: /en/v0.4.23/ -Disallow: /en/v0.4.24/ -Disallow: /en/v0.4.25/ -Disallow: /en/v0.4.26/ -Disallow: /en/v0.5.0/ -Disallow: /en/v0.5.1/ -Disallow: /en/v0.5.2/ -Disallow: /en/v0.5.3/ -Disallow: /en/v0.5.4/ -Disallow: /en/v0.5.5/ -Disallow: /en/v0.5.6/ -Disallow: /en/v0.5.7/ -Disallow: /en/v0.5.8/ -Disallow: /en/v0.5.9/ -Disallow: /en/v0.5.10/ -Disallow: /en/v0.5.11/ -Disallow: /en/v0.5.12/ -Disallow: /en/v0.5.13/ -Disallow: /en/v0.5.14/ -Disallow: /en/v0.5.15/ -Disallow: /en/v0.5.16/ -Disallow: /en/v0.5.17/ -Disallow: /en/v0.6.0/ -Disallow: /en/v0.6.1/ -Disallow: /en/v0.6.2/ -Disallow: /en/v0.6.3/ -Disallow: /en/v0.6.4/ -Disallow: /en/v0.6.5/ -Disallow: /en/v0.6.6/ -Disallow: /en/v0.6.7/ -Disallow: /en/v0.6.8/ -Disallow: /en/v0.6.9/ -Disallow: /en/v0.6.10/ -Disallow: /en/v0.6.11/ -Disallow: /en/v0.6.12/ -Disallow: /en/v0.7.0/ -Disallow: /en/v0.7.1/ -Disallow: /en/v0.7.2/ -Disallow: /en/v0.7.3/ -Disallow: /en/v0.7.4/ -Disallow: /en/v0.7.5/ -# Allow the last patch release of the 0.7.x series -#Disallow: /en/v0.7.6/ -Disallow: /en/v0.8.0/ -Disallow: /en/v0.8.1/ -Disallow: /en/v0.8.2/ -# Allow the latest release -#Disallow: /en/v0.8.3/ diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html index 1db4ef92c..5f81b2320 100644 --- a/docs/_templates/layout.html +++ b/docs/_templates/layout.html @@ -2,5 +2,5 @@ {% block menu %} {{ super() }} - Keyword Index + Keyword Index {% endblock %} diff --git a/docs/abi-spec.rst b/docs/abi-spec.rst index f7b3f88e7..d412a2a2e 100644 --- a/docs/abi-spec.rst +++ b/docs/abi-spec.rst @@ -235,7 +235,7 @@ Examples Given the contract: -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0; @@ -465,17 +465,20 @@ address, a series of up to four topics and some arbitrary length binary data. Ev ABI in order to interpret this (together with an interface spec) as a properly typed structure. Given an event name and series of event parameters, we split them into two sub-series: those which are indexed and -those which are not. Those which are indexed, which may number up to 3, are used alongside the Keccak hash of the -event signature to form the topics of the log entry. Those which are not indexed form the byte array of the event. +those which are not. +Those which are indexed, which may number up to 3 (for non-anonymous events) or 4 (for anonymous ones), are used +alongside the Keccak hash of the event signature to form the topics of the log entry. +Those which are not indexed form the byte array of the event. In effect, a log entry using this ABI is described as: - ``address``: the address of the contract (intrinsically provided by Ethereum); - ``topics[0]``: ``keccak(EVENT_NAME+"("+EVENT_ARGS.map(canonical_type_of).join(",")+")")`` (``canonical_type_of`` is a function that simply returns the canonical type of a given argument, e.g. for ``uint indexed foo``, it would - return ``uint256``). If the event is declared as ``anonymous`` the ``topics[0]`` is not generated; -- ``topics[n]``: ``abi_encode(EVENT_INDEXED_ARGS[n - 1])`` (``EVENT_INDEXED_ARGS`` is the series of ``EVENT_ARGS`` - that are indexed); + return ``uint256``). This value is only present in ``topics[0]`` if the event is not declared as ``anonymous``; +- ``topics[n]``: ``abi_encode(EVENT_INDEXED_ARGS[n - 1])`` if the event is not declared as ``anonymous`` + or ``abi_encode(EVENT_INDEXED_ARGS[n])`` if it is (``EVENT_INDEXED_ARGS`` is the series of ``EVENT_ARGS`` that + are indexed); - ``data``: ABI encoding of ``EVENT_NON_INDEXED_ARGS`` (``EVENT_NON_INDEXED_ARGS`` is the series of ``EVENT_ARGS`` that are not indexed, ``abi_encode`` is the ABI encoding function used for returning a series of typed values from a function, as described above). @@ -507,7 +510,7 @@ call. As an example, let us consider the following contract whose ``transfer`` function always reverts with a custom error of "insufficient balance": -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.4; @@ -596,7 +599,7 @@ Errors look as follows: For example, -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.4; @@ -650,7 +653,7 @@ which is of array type and has the same structure as the top-level object except As an example, the code -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >0.7.4 <0.9.0; diff --git a/docs/assembly.rst b/docs/assembly.rst index d5d73cfb1..4e35ca702 100644 --- a/docs/assembly.rst +++ b/docs/assembly.rst @@ -39,7 +39,7 @@ load it into a ``bytes`` variable. This is not possible with "plain Solidity" an idea is that reusable assembly libraries can enhance the Solidity language without a compiler change. -.. code:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0; @@ -65,7 +65,7 @@ without a compiler change. Inline assembly is also beneficial in cases where the optimizer fails to produce efficient code, for example: -.. code:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0; @@ -141,7 +141,8 @@ Both expressions can also be assigned to. Local Solidity variables are available for assignments, for example: -.. code:: +.. code-block:: solidity + :force: // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; @@ -216,7 +217,9 @@ starting from where this pointer points at and update it. There is no guarantee that the memory has not been used before and thus you cannot assume that its contents are zero bytes. There is no built-in mechanism to release or free allocated memory. -Here is an assembly snippet you can use for allocating memory that follows the process outlined above:: +Here is an assembly snippet you can use for allocating memory that follows the process outlined above + +.. code-block:: yul function allocate(length) -> pos { pos := mload(0x40) diff --git a/docs/bugs.json b/docs/bugs.json index 36d800b43..5f394aecc 100644 --- a/docs/bugs.json +++ b/docs/bugs.json @@ -1,8 +1,10 @@ [ { + "uid": "SOL-2021-2", "name": "ABIDecodeTwoDimensionalArrayMemory", "summary": "If used on memory byte arrays, result of the function ``abi.decode`` can depend on the contents of memory outside of the actual byte array that is decoded.", "description": "The ABI specification uses pointers to data areas for everything that is dynamically-sized. When decoding data from memory (instead of calldata), the ABI decoder did not properly validate some of these pointers. More specifically, it was possible to use large values for the pointers inside arrays such that computing the offset resulted in an undetected overflow. This could lead to these pointers targeting areas in memory outside of the actual area to be decoded. This way, it was possible for ``abi.decode`` to return different values for the same encoded byte array.", + "link": "https://blog.soliditylang.org/2021/04/21/decoding-from-memory-bug/", "introduced": "0.4.16", "fixed": "0.8.4", "conditions": { @@ -11,6 +13,7 @@ "severity": "very low" }, { + "uid": "SOL-2021-1", "name": "KeccakCaching", "summary": "The bytecode optimizer incorrectly re-used previously evaluated Keccak-256 hashes. You are unlikely to be affected if you do not compute Keccak-256 hashes in inline assembly.", "description": "Solidity's bytecode optimizer has a step that can compute Keccak-256 hashes, if the contents of the memory are known during compilation time. This step also has a mechanism to determine that two Keccak-256 hashes are equal even if the values in memory are not known during compile time. This mechanism had a bug where Keccak-256 of the same memory content, but different sizes were considered equal. More specifically, ``keccak256(mpos1, length1)`` and ``keccak256(mpos2, length2)`` in some cases were considered equal if ``length1`` and ``length2``, when rounded up to nearest multiple of 32 were the same, and when the memory contents at ``mpos1`` and ``mpos2`` can be deduced to be equal. You maybe affected if you compute multiple Keccak-256 hashes of the same content, but with different lengths inside inline assembly. You are unaffected if your code uses ``keccak256`` with a length that is not a compile-time constant or if it is always a multiple of 32.", @@ -22,20 +25,25 @@ "severity": "medium" }, { + "uid": "SOL-2020-11", "name": "EmptyByteArrayCopy", "summary": "Copying an empty byte array (or string) from memory or calldata to storage can result in data corruption if the target array's length is increased subsequently without storing new data.", "description": "The routine that copies byte arrays from memory or calldata to storage stores unrelated data from after the source array in the storage slot if the source array is empty. If the storage array's length is subsequently increased either by using ``.push()`` or by assigning to its ``.length`` attribute (only before 0.6.0), the newly created byte array elements will not be zero-initialized, but contain the unrelated data. You are not affected if you do not assign to ``.length`` and do not use ``.push()`` on byte arrays, or only use ``.push()`` or manually initialize the new elements.", + "link": "https://blog.soliditylang.org/2020/10/19/empty-byte-array-copy-bug/", "fixed": "0.7.4", "severity": "medium" }, { + "uid": "SOL-2020-10", "name": "DynamicArrayCleanup", "summary": "When assigning a dynamically-sized array with types of size at most 16 bytes in storage causing the assigned array to shrink, some parts of deleted slots were not zeroed out.", "description": "Consider a dynamically-sized array in storage whose base-type is small enough such that multiple values can be packed into a single slot, such as `uint128[]`. Let us define its length to be `l`. When this array gets assigned from another array with a smaller length, say `m`, the slots between elements `m` and `l` have to be cleaned by zeroing them out. However, this cleaning was not performed properly. Specifically, after the slot corresponding to `m`, only the first packed value was cleaned up. If this array gets resized to a length larger than `m`, the indices corresponding to the unclean parts of the slot contained the original value, instead of 0. The resizing here is performed by assigning to the array `length`, by a `push()` or via inline assembly. You are not affected if you are only using `.push()` or if you assign a value (even zero) to the new elements after increasing the length of the array.", + "link": "https://blog.soliditylang.org/2020/10/07/solidity-dynamic-array-cleanup-bug/", "fixed": "0.7.3", "severity": "medium" }, { + "uid": "SOL-2020-9", "name": "FreeFunctionRedefinition", "summary": "The compiler does not flag an error when two or more free functions with the same name and parameter types are defined in a source unit or when an imported free function alias shadows another free function with a different name but identical parameter types.", "description": "In contrast to functions defined inside contracts, free functions with identical names and parameter types did not create an error. Both definition of free functions with identical name and parameter types and an imported free function with an alias that shadows another function with a different name but identical parameter types were permitted due to which a call to either the multiply defined free function or the imported free function alias within a contract led to the execution of that free function which was defined first within the source unit. Subsequently defined identical free function definitions were silently ignored and their code generation was skipped.", @@ -44,6 +52,7 @@ "severity": "low" }, { + "uid": "SOL-2020-8", "name": "UsingForCalldata", "summary": "Function calls to internal library functions with calldata parameters called via ``using for`` can result in invalid data being read.", "description": "Function calls to internal library functions using the ``using for`` mechanism copied all calldata parameters to memory first and passed them on like that, regardless of whether it was an internal or an external call. Due to that, the called function would receive a memory pointer that is interpreted as a calldata pointer. Since dynamically sized arrays are passed using two stack slots for calldata, but only one for memory, this can lead to stack corruption. An affected library call will consider the JUMPDEST to which it is supposed to return as part of its arguments and will instead jump out to whatever was on the stack before the call.", @@ -52,6 +61,7 @@ "severity": "very low" }, { + "uid": "SOL-2020-7", "name": "MissingEscapingInFormatting", "summary": "String literals containing double backslash characters passed directly to external or encoding function calls can lead to a different string being used when ABIEncoderV2 is enabled.", "description": "When ABIEncoderV2 is enabled, string literals passed directly to encoding functions or external function calls are stored as strings in the intemediate code. Characters outside the printable range are handled correctly, but backslashes are not escaped in this procedure. This leads to double backslashes being reduced to single backslashes and consequently re-interpreted as escapes potentially resulting in a different string being encoded.", @@ -63,6 +73,7 @@ } }, { + "uid": "SOL-2020-6", "name": "ArraySliceDynamicallyEncodedBaseType", "summary": "Accessing array slices of arrays with dynamically encoded base types (e.g. multi-dimensional arrays) can result in invalid data being read.", "description": "For arrays with dynamically sized base types, index range accesses that use a start expression that is non-zero will result in invalid array slices. Any index access to such array slices will result in data being read from incorrect calldata offsets. Array slices are only supported for dynamic calldata types and all problematic type require ABIEncoderV2 to be enabled.", @@ -74,6 +85,7 @@ } }, { + "uid": "SOL-2020-5", "name": "ImplicitConstructorCallvalueCheck", "summary": "The creation code of a contract that does not define a constructor but has a base that does define a constructor did not revert for calls with non-zero value.", "description": "Starting from Solidity 0.4.5 the creation code of contracts without explicit payable constructor is supposed to contain a callvalue check that results in contract creation reverting, if non-zero value is passed. However, this check was missing in case no explicit constructor was defined in a contract at all, but the contract has a base that does define a constructor. In these cases it is possible to send value in a contract creation transaction or using inline assembly without revert, even though the creation code is supposed to be non-payable.", @@ -82,6 +94,7 @@ "severity": "very low" }, { + "uid": "SOL-2020-4", "name": "TupleAssignmentMultiStackSlotComponents", "summary": "Tuple assignments with components that occupy several stack slots, i.e. nested tuples, pointers to external functions or references to dynamically sized calldata arrays, can result in invalid values.", "description": "Tuple assignments did not correctly account for tuple components that occupy multiple stack slots in case the number of stack slots differs between left-hand-side and right-hand-side. This can either happen in the presence of nested tuples or if the right-hand-side contains external function pointers or references to dynamic calldata arrays, while the left-hand-side contains an omission.", @@ -90,15 +103,17 @@ "severity": "very low" }, { + "uid": "SOL-2020-3", "name": "MemoryArrayCreationOverflow", "summary": "The creation of very large memory arrays can result in overlapping memory regions and thus memory corruption.", "description": "No runtime overflow checks were performed for the length of memory arrays during creation. In cases for which the memory size of an array in bytes, i.e. the array length times 32, is larger than 2^256-1, the memory allocation will overflow, potentially resulting in overlapping memory areas. The length of the array is still stored correctly, so copying or iterating over such an array will result in out-of-gas.", - "link": "https://solidity.ethereum.org/2020/04/06/memory-creation-overflow-bug/", + "link": "https://blog.soliditylang.org/2020/04/06/memory-creation-overflow-bug/", "introduced": "0.2.0", "fixed": "0.6.5", "severity": "low" }, { + "uid": "SOL-2020-1", "name": "YulOptimizerRedundantAssignmentBreakContinue", "summary": "The Yul optimizer can remove essential assignments to variables declared inside for loops when Yul's continue or break statement is used. You are unlikely to be affected if you do not use inline assembly with for loops and continue and break statements.", "description": "The Yul optimizer has a stage that removes assignments to variables that are overwritten again or are not used in all following control-flow branches. This logic incorrectly removes such assignments to variables declared inside a for loop if they can be removed in a control-flow branch that ends with ``break`` or ``continue`` even though they cannot be removed in other control-flow branches. Variables declared outside of the respective for loop are not affected.", @@ -110,6 +125,7 @@ } }, { + "uid": "SOL-2020-2", "name": "privateCanBeOverridden", "summary": "Private methods can be overridden by inheriting contracts.", "description": "While private methods of base contracts are not visible and cannot be called directly from the derived contract, it is still possible to declare a function of the same name and type and thus change the behaviour of the base contract's function.", @@ -118,6 +134,7 @@ "severity": "low" }, { + "uid": "SOL-2020-1", "name": "YulOptimizerRedundantAssignmentBreakContinue0.5", "summary": "The Yul optimizer can remove essential assignments to variables declared inside for loops when Yul's continue or break statement is used. You are unlikely to be affected if you do not use inline assembly with for loops and continue and break statements.", "description": "The Yul optimizer has a stage that removes assignments to variables that are overwritten again or are not used in all following control-flow branches. This logic incorrectly removes such assignments to variables declared inside a for loop if they can be removed in a control-flow branch that ends with ``break`` or ``continue`` even though they cannot be removed in other control-flow branches. Variables declared outside of the respective for loop are not affected.", @@ -129,6 +146,7 @@ } }, { + "uid": "SOL-2019-10", "name": "ABIEncoderV2LoopYulOptimizer", "summary": "If both the experimental ABIEncoderV2 and the experimental Yul optimizer are activated, one component of the Yul optimizer may reuse data in memory that has been changed in the meantime.", "description": "The Yul optimizer incorrectly replaces ``mload`` and ``sload`` calls with values that have been previously written to the load location (and potentially changed in the meantime) if all of the following conditions are met: (1) there is a matching ``mstore`` or ``sstore`` call before; (2) the contents of memory or storage is only changed in a function that is called (directly or indirectly) in between the first store and the load call; (3) called function contains a for loop where the same memory location is changed in the condition or the post or body block. When used in Solidity mode, this can only happen if the experimental ABIEncoderV2 is activated and the experimental Yul optimizer has been activated manually in addition to the regular optimizer in the compiler settings.", @@ -142,6 +160,7 @@ } }, { + "uid": "SOL-2019-9", "name": "ABIEncoderV2CalldataStructsWithStaticallySizedAndDynamicallyEncodedMembers", "summary": "Reading from calldata structs that contain dynamically encoded, but statically-sized members can result in incorrect values.", "description": "When a calldata struct contains a dynamically encoded, but statically-sized member, the offsets for all subsequent struct members are calculated incorrectly. All reads from such members will result in invalid values. Only calldata structs are affected, i.e. this occurs in external functions with such structs as argument. Using affected structs in storage or memory or as arguments to public functions on the other hand works correctly.", @@ -153,19 +172,21 @@ } }, { + "uid": "SOL-2019-8", "name": "SignedArrayStorageCopy", "summary": "Assigning an array of signed integers to a storage array of different type can lead to data corruption in that array.", "description": "In two's complement, negative integers have their higher order bits set. In order to fit into a shared storage slot, these have to be set to zero. When a conversion is done at the same time, the bits to set to zero were incorrectly determined from the source and not the target type. This means that such copy operations can lead to incorrect values being stored.", - "link": "https://blog.ethereum.org/2019/06/25/solidity-storage-array-bugs/", + "link": "https://blog.soliditylang.org/2019/06/25/solidity-storage-array-bugs/", "introduced": "0.4.7", "fixed": "0.5.10", "severity": "low/medium" }, { + "uid": "SOL-2019-7", "name": "ABIEncoderV2StorageArrayWithMultiSlotElement", "summary": "Storage arrays containing structs or other statically-sized arrays are not read properly when directly encoded in external function calls or in abi.encode*.", "description": "When storage arrays whose elements occupy more than a single storage slot are directly encoded in external function calls or using abi.encode*, their elements are read in an overlapping manner, i.e. the element pointer is not properly advanced between reads. This is not a problem when the storage data is first copied to a memory variable or if the storage array only contains value types or dynamically-sized arrays.", - "link": "https://blog.ethereum.org/2019/06/25/solidity-storage-array-bugs/", + "link": "https://blog.soliditylang.org/2019/06/25/solidity-storage-array-bugs/", "introduced": "0.4.16", "fixed": "0.5.10", "severity": "low", @@ -174,6 +195,7 @@ } }, { + "uid": "SOL-2019-6", "name": "DynamicConstructorArgumentsClippedABIV2", "summary": "A contract's constructor that takes structs or arrays that contain dynamically-sized arrays reverts or decodes to invalid data.", "description": "During construction of a contract, constructor parameters are copied from the code section to memory for decoding. The amount of bytes to copy was calculated incorrectly in case all parameters are statically-sized but contain dynamically-sized arrays as struct members or inner arrays. Such types are only available if ABIEncoderV2 is activated.", @@ -185,6 +207,7 @@ } }, { + "uid": "SOL-2019-5", "name": "UninitializedFunctionPointerInConstructor", "summary": "Calling uninitialized internal function pointers created in the constructor does not always revert and can cause unexpected behaviour.", "description": "Uninitialized internal function pointers point to a special piece of code that causes a revert when called. Jump target positions are different during construction and after deployment, but the code for setting this special jump target only considered the situation after deployment.", @@ -193,6 +216,7 @@ "severity": "very low" }, { + "uid": "SOL-2019-5", "name": "UninitializedFunctionPointerInConstructor_0.4.x", "summary": "Calling uninitialized internal function pointers created in the constructor does not always revert and can cause unexpected behaviour.", "description": "Uninitialized internal function pointers point to a special piece of code that causes a revert when called. Jump target positions are different during construction and after deployment, but the code for setting this special jump target only considered the situation after deployment.", @@ -201,6 +225,7 @@ "severity": "very low" }, { + "uid": "SOL-2019-4", "name": "IncorrectEventSignatureInLibraries", "summary": "Contract types used in events in libraries cause an incorrect event signature hash", "description": "Instead of using the type `address` in the hashed signature, the actual contract name was used, leading to a wrong hash in the logs.", @@ -209,6 +234,7 @@ "severity": "very low" }, { + "uid": "SOL-2019-4", "name": "IncorrectEventSignatureInLibraries_0.4.x", "summary": "Contract types used in events in libraries cause an incorrect event signature hash", "description": "Instead of using the type `address` in the hashed signature, the actual contract name was used, leading to a wrong hash in the logs.", @@ -217,10 +243,11 @@ "severity": "very low" }, { + "uid": "SOL-2019-3", "name": "ABIEncoderV2PackedStorage", "summary": "Storage structs and arrays with types shorter than 32 bytes can cause data corruption if encoded directly from storage using the experimental ABIEncoderV2.", "description": "Elements of structs and arrays that are shorter than 32 bytes are not properly decoded from storage when encoded directly (i.e. not via a memory type) using ABIEncoderV2. This can cause corruption in the values themselves but can also overwrite other parts of the encoded data.", - "link": "https://blog.ethereum.org/2019/03/26/solidity-optimizer-and-abiencoderv2-bug/", + "link": "https://blog.soliditylang.org/2019/03/26/solidity-optimizer-and-abiencoderv2-bug/", "introduced": "0.5.0", "fixed": "0.5.7", "severity": "low", @@ -229,10 +256,11 @@ } }, { + "uid": "SOL-2019-3", "name": "ABIEncoderV2PackedStorage_0.4.x", "summary": "Storage structs and arrays with types shorter than 32 bytes can cause data corruption if encoded directly from storage using the experimental ABIEncoderV2.", "description": "Elements of structs and arrays that are shorter than 32 bytes are not properly decoded from storage when encoded directly (i.e. not via a memory type) using ABIEncoderV2. This can cause corruption in the values themselves but can also overwrite other parts of the encoded data.", - "link": "https://blog.ethereum.org/2019/03/26/solidity-optimizer-and-abiencoderv2-bug/", + "link": "https://blog.soliditylang.org/2019/03/26/solidity-optimizer-and-abiencoderv2-bug/", "introduced": "0.4.19", "fixed": "0.4.26", "severity": "low", @@ -241,10 +269,11 @@ } }, { + "uid": "SOL-2019-2", "name": "IncorrectByteInstructionOptimization", "summary": "The optimizer incorrectly handles byte opcodes whose second argument is 31 or a constant expression that evaluates to 31. This can result in unexpected values.", "description": "The optimizer incorrectly handles byte opcodes that use the constant 31 as second argument. This can happen when performing index access on bytesNN types with a compile-time constant value (not index) of 31 or when using the byte opcode in inline assembly.", - "link": "https://blog.ethereum.org/2019/03/26/solidity-optimizer-and-abiencoderv2-bug/", + "link": "https://blog.soliditylang.org/2019/03/26/solidity-optimizer-and-abiencoderv2-bug/", "introduced": "0.5.5", "fixed": "0.5.7", "severity": "very low", @@ -253,10 +282,11 @@ } }, { + "uid": "SOL-2019-1", "name": "DoubleShiftSizeOverflow", "summary": "Double bitwise shifts by large constants whose sum overflows 256 bits can result in unexpected values.", "description": "Nested logical shift operations whose total shift size is 2**256 or more are incorrectly optimized. This only applies to shifts by numbers of bits that are compile-time constant expressions.", - "link": "https://blog.ethereum.org/2019/03/26/solidity-optimizer-and-abiencoderv2-bug/", + "link": "https://blog.soliditylang.org/2019/03/26/solidity-optimizer-and-abiencoderv2-bug/", "introduced": "0.5.5", "fixed": "0.5.6", "severity": "low", @@ -266,35 +296,39 @@ } }, { + "uid": "SOL-2018-4", "name": "ExpExponentCleanup", "summary": "Using the ** operator with an exponent of type shorter than 256 bits can result in unexpected values.", "description": "Higher order bits in the exponent are not properly cleaned before the EXP opcode is applied if the type of the exponent expression is smaller than 256 bits and not smaller than the type of the base. In that case, the result might be larger than expected if the exponent is assumed to lie within the value range of the type. Literal numbers as exponents are unaffected as are exponents or bases of type uint256.", - "link": "https://blog.ethereum.org/2018/09/13/solidity-bugfix-release/", + "link": "https://blog.soliditylang.org/2018/09/13/solidity-bugfix-release/", "fixed": "0.4.25", "severity": "medium/high", "check": {"regex-source": "[^/]\\*\\* *[^/0-9 ]"} }, { + "uid": "SOL-2018-3", "name": "EventStructWrongData", "summary": "Using structs in events logged wrong data.", "description": "If a struct is used in an event, the address of the struct is logged instead of the actual data.", - "link": "https://blog.ethereum.org/2018/09/13/solidity-bugfix-release/", + "link": "https://blog.soliditylang.org/2018/09/13/solidity-bugfix-release/", "introduced": "0.4.17", "fixed": "0.4.25", "severity": "very low", "check": {"ast-compact-json-path": "$..[?(@.nodeType === 'EventDefinition')]..[?(@.nodeType === 'UserDefinedTypeName' && @.typeDescriptions.typeString.startsWith('struct'))]"} }, { + "uid": "SOL-2018-2", "name": "NestedArrayFunctionCallDecoder", "summary": "Calling functions that return multi-dimensional fixed-size arrays can result in memory corruption.", "description": "If Solidity code calls a function that returns a multi-dimensional fixed-size array, array elements are incorrectly interpreted as memory pointers and thus can cause memory corruption if the return values are accessed. Calling functions with multi-dimensional fixed-size arrays is unaffected as is returning fixed-size arrays from function calls. The regular expression only checks if such functions are present, not if they are called, which is required for the contract to be affected.", - "link": "https://blog.ethereum.org/2018/09/13/solidity-bugfix-release/", + "link": "https://blog.soliditylang.org/2018/09/13/solidity-bugfix-release/", "introduced": "0.1.4", "fixed": "0.4.22", "severity": "medium", "check": {"regex-source": "returns[^;{]*\\[\\s*[^\\] \\t\\r\\n\\v\\f][^\\]]*\\]\\s*\\[\\s*[^\\] \\t\\r\\n\\v\\f][^\\]]*\\][^{;]*[;{]"} }, { + "uid": "SOL-2018-1", "name": "OneOfTwoConstructorsSkipped", "summary": "If a contract has both a new-style constructor (using the constructor keyword) and an old-style constructor (a function with the same name as the contract) at the same time, one of them will be ignored.", "description": "If a contract has both a new-style constructor (using the constructor keyword) and an old-style constructor (a function with the same name as the contract) at the same time, one of them will be ignored. There will be a compiler warning about the old-style constructor, so contracts only using new-style constructors are fine.", @@ -303,6 +337,7 @@ "severity": "very low" }, { + "uid": "SOL-2017-5", "name": "ZeroFunctionSelector", "summary": "It is possible to craft the name of a function such that it is executed instead of the fallback function in very specific circumstances.", "description": "If a function has a selector consisting only of zeros, is payable and part of a contract that does not have a fallback function and at most five external functions in total, this function is called instead of the fallback function if Ether is sent to the contract without data.", @@ -310,6 +345,7 @@ "severity": "very low" }, { + "uid": "SOL-2017-4", "name": "DelegateCallReturnValue", "summary": "The low-level .delegatecall() does not return the execution outcome, but converts the value returned by the functioned called to a boolean instead.", "description": "The return value of the low-level .delegatecall() function is taken from a position in memory, where the call data or the return data resides. This value is interpreted as a boolean and put onto the stack. This means if the called function returns at least 32 zero bytes, .delegatecall() returns false even if the call was successful.", @@ -318,6 +354,7 @@ "severity": "low" }, { + "uid": "SOL-2017-3", "name": "ECRecoverMalformedInput", "summary": "The ecrecover() builtin can return garbage for malformed input.", "description": "The ecrecover precompile does not properly signal failure for malformed input (especially in the 'v' argument) and thus the Solidity function can return data that was previously present in the return area in memory.", @@ -325,6 +362,7 @@ "severity": "medium" }, { + "uid": "SOL-2017-2", "name": "SkipEmptyStringLiteral", "summary": "If \"\" is used in a function call, the following function arguments will not be correctly passed to the function.", "description": "If the empty string literal \"\" is used as an argument in a function call, it is skipped by the encoder. This has the effect that the encoding of all arguments following this is shifted left by 32 bytes and thus the function call data is corrupted.", @@ -332,10 +370,11 @@ "severity": "low" }, { + "uid": "SOL-2017-1", "name": "ConstantOptimizerSubtraction", "summary": "In some situations, the optimizer replaces certain numbers in the code with routines that compute different numbers.", "description": "The optimizer tries to represent any number in the bytecode by routines that compute them with less gas. For some special numbers, an incorrect routine is generated. This could allow an attacker to e.g. trick victims about a specific amount of ether, or function calls to call different functions (or none at all).", - "link": "https://blog.ethereum.org/2017/05/03/solidity-optimizer-bug/", + "link": "https://blog.soliditylang.org/2017/05/03/solidity-optimizer-bug/", "fixed": "0.4.11", "severity": "low", "conditions": { @@ -343,6 +382,7 @@ } }, { + "uid": "SOL-2016-11", "name": "IdentityPrecompileReturnIgnored", "summary": "Failure of the identity precompile was ignored.", "description": "Calls to the identity contract, which is used for copying memory, ignored its return value. On the public chain, calls to the identity precompile can be made in a way that they never fail, but this might be different on private chains.", @@ -350,6 +390,7 @@ "fixed": "0.4.7" }, { + "uid": "SOL-2016-10", "name": "OptimizerStateKnowledgeNotResetForJumpdest", "summary": "The optimizer did not properly reset its internal state at jump destinations, which could lead to data corruption.", "description": "The optimizer performs symbolic execution at certain stages. At jump destinations, multiple code paths join and thus it has to compute a common state from the incoming edges. Computing this common state was simplified to just use the empty state, but this implementation was not done properly. This bug can cause data corruption.", @@ -361,15 +402,17 @@ } }, { + "uid": "SOL-2016-9", "name": "HighOrderByteCleanStorage", "summary": "For short types, the high order bytes were not cleaned properly and could overwrite existing data.", "description": "Types shorter than 32 bytes are packed together into the same 32 byte storage slot, but storage writes always write 32 bytes. For some types, the higher order bytes were not cleaned properly, which made it sometimes possible to overwrite a variable in storage when writing to another one.", - "link": "https://blog.ethereum.org/2016/11/01/security-alert-solidity-variables-can-overwritten-storage/", + "link": "https://blog.soliditylang.org/2016/11/01/security-alert-solidity-variables-can-overwritten-storage/", "severity": "high", "introduced": "0.1.6", "fixed": "0.4.4" }, { + "uid": "SOL-2016-8", "name": "OptimizerStaleKnowledgeAboutSHA3", "summary": "The optimizer did not properly reset its knowledge about SHA3 operations resulting in some hashes (also used for storage variable positions) not being calculated correctly.", "description": "The optimizer performs symbolic execution in order to save re-evaluating expressions whose value is already known. This knowledge was not properly reset across control flow paths and thus the optimizer sometimes thought that the result of a SHA3 operation is already present on the stack. This could result in data corruption by accessing the wrong storage slot.", @@ -380,6 +423,7 @@ } }, { + "uid": "SOL-2016-7", "name": "LibrariesNotCallableFromPayableFunctions", "summary": "Library functions threw an exception when called from a call that received Ether.", "description": "Library functions are protected against sending them Ether through a call. Since the DELEGATECALL opcode forwards the information about how much Ether was sent with a call, the library function incorrectly assumed that Ether was sent to the library and threw an exception.", @@ -388,6 +432,7 @@ "fixed": "0.4.2" }, { + "uid": "SOL-2016-6", "name": "SendFailsForZeroEther", "summary": "The send function did not provide enough gas to the recipient if no Ether was sent with it.", "description": "The recipient of an Ether transfer automatically receives a certain amount of gas from the EVM to handle the transfer. In the case of a zero-transfer, this gas is not provided which causes the recipient to throw an exception.", @@ -395,6 +440,7 @@ "fixed": "0.4.0" }, { + "uid": "SOL-2016-5", "name": "DynamicAllocationInfiniteLoop", "summary": "Dynamic allocation of an empty memory array caused an infinite loop and thus an exception.", "description": "Memory arrays can be created provided a length. If this length is zero, code was generated that did not terminate and thus consumed all gas.", @@ -402,6 +448,7 @@ "fixed": "0.3.6" }, { + "uid": "SOL-2016-4", "name": "OptimizerClearStateOnCodePathJoin", "summary": "The optimizer did not properly reset its internal state at jump destinations, which could lead to data corruption.", "description": "The optimizer performs symbolic execution at certain stages. At jump destinations, multiple code paths join and thus it has to compute a common state from the incoming edges. Computing this common state was not done correctly. This bug can cause data corruption, but it is probably quite hard to use for targeted attacks.", @@ -412,6 +459,7 @@ } }, { + "uid": "SOL-2016-3", "name": "CleanBytesHigherOrderBits", "summary": "The higher order bits of short bytesNN types were not cleaned before comparison.", "description": "Two variables of type bytesNN were considered different if their higher order bits, which are not part of the actual value, were different. An attacker might use this to reach seemingly unreachable code paths by providing incorrectly formatted input data.", @@ -419,6 +467,7 @@ "fixed": "0.3.3" }, { + "uid": "SOL-2016-2", "name": "ArrayAccessCleanHigherOrderBits", "summary": "Access to array elements for arrays of types with less than 32 bytes did not correctly clean the higher order bits, causing corruption in other array elements.", "description": "Multiple elements of an array of values that are shorter than 17 bytes are packed into the same storage slot. Writing to a single element of such an array did not properly clean the higher order bytes and thus could lead to data corruption.", @@ -426,6 +475,7 @@ "fixed": "0.3.1" }, { + "uid": "SOL-2016-1", "name": "AncientCompiler", "summary": "This compiler version is ancient and might contain several undocumented or undiscovered bugs.", "description": "The list of bugs is only kept for compiler versions starting from 0.3.0, so older versions might contain undocumented bugs.", diff --git a/docs/bugs.rst b/docs/bugs.rst index 11680abcd..73700adf3 100644 --- a/docs/bugs.rst +++ b/docs/bugs.rst @@ -33,6 +33,10 @@ contracts should consult this list according to the following criteria: The JSON file of known bugs below is an array of objects, one for each bug, with the following keys: +uid + Unique identifier given to the bug in the form of ``SOL--``. + It is possible that multiple entries exists with the same uid. This means + multiple version ranges are affected by the same bug. name Unique name given to the bug summary diff --git a/docs/bugs_by_version.json b/docs/bugs_by_version.json index dd6706872..9f708595d 100644 --- a/docs/bugs_by_version.json +++ b/docs/bugs_by_version.json @@ -1543,5 +1543,13 @@ "0.8.4": { "bugs": [], "released": "2021-04-21" + }, + "0.8.5": { + "bugs": [], + "released": "2021-06-10" + }, + "0.8.6": { + "bugs": [], + "released": "2021-06-22" } } \ No newline at end of file diff --git a/docs/common-patterns.rst b/docs/common-patterns.rst index a7b276661..58fb39ea2 100644 --- a/docs/common-patterns.rst +++ b/docs/common-patterns.rst @@ -25,7 +25,7 @@ contract in order to become the "richest", inspired by In the following contract, if you are no longer the richest, you receive the funds of the person who is now the richest. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.4; @@ -63,7 +63,7 @@ you receive the funds of the person who is now the richest. This is as opposed to the more intuitive sending pattern: -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.4; @@ -129,7 +129,7 @@ functions and this is what this section is about. The use of **function modifiers** makes these restrictions highly readable. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.4; @@ -292,7 +292,7 @@ function finishes. Starting with version 0.4.0, modifier code will run even if the function explicitly returns. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.4; diff --git a/docs/conf.py b/docs/conf.py index f0eb06e4d..4c7a67164 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -26,10 +26,10 @@ from pygments_lexer_solidity import SolidityLexer, YulLexer def setup(sphinx): thisdir = os.path.dirname(os.path.realpath(__file__)) sys.path.insert(0, thisdir + '/utils') - sphinx.add_lexer('Solidity', SolidityLexer()) - sphinx.add_lexer('Yul', YulLexer()) + sphinx.add_lexer('Solidity', SolidityLexer) + sphinx.add_lexer('Yul', YulLexer) - sphinx.add_stylesheet('css/custom.css') + sphinx.add_css_file('css/custom.css') # -- General configuration ------------------------------------------------ diff --git a/docs/contracts/abstract-contracts.rst b/docs/contracts/abstract-contracts.rst index 8467337af..34792f58c 100644 --- a/docs/contracts/abstract-contracts.rst +++ b/docs/contracts/abstract-contracts.rst @@ -11,7 +11,9 @@ Contracts may be marked as abstract even though all functions are implemented. This can be done by using the ``abstract`` keyword as shown in the following example. Note that this contract needs to be defined as abstract, because the function ``utterance()`` was defined, but no implementation was -provided (no implementation body ``{ }`` was given).:: +provided (no implementation body ``{ }`` was given). + +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.0 <0.9.0; @@ -21,7 +23,9 @@ provided (no implementation body ``{ }`` was given).:: } Such abstract contracts can not be instantiated directly. This is also true, if an abstract contract itself does implement -all defined functions. The usage of an abstract contract as a base class is shown in the following example:: +all defined functions. The usage of an abstract contract as a base class is shown in the following example: + +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.0 <0.9.0; @@ -40,11 +44,15 @@ functions by overriding, it needs to be marked as abstract as well. Note that a function without implementation is different from a :ref:`Function Type ` even though their syntax looks very similar. -Example of function without implementation (a function declaration):: +Example of function without implementation (a function declaration): + +.. code-block:: solidity function foo(address) external returns (address); -Example of a declaration of a variable whose type is a function type:: +Example of a declaration of a variable whose type is a function type: + +.. code-block:: solidity function(address) external returns (address) foo; diff --git a/docs/contracts/constant-state-variables.rst b/docs/contracts/constant-state-variables.rst index d22735618..01a4f164c 100644 --- a/docs/contracts/constant-state-variables.rst +++ b/docs/contracts/constant-state-variables.rst @@ -27,7 +27,7 @@ can sometimes be cheaper than immutable values. Not all types for constants and immutables are implemented at this time. The only supported types are :ref:`strings ` (only for constants) and :ref:`value types `. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.4; diff --git a/docs/contracts/creating-contracts.rst b/docs/contracts/creating-contracts.rst index d93ac832c..f1fd3b0b5 100644 --- a/docs/contracts/creating-contracts.rst +++ b/docs/contracts/creating-contracts.rst @@ -32,7 +32,7 @@ If a contract wants to create another contract, the source code (and the binary) of the created contract has to be known to the creator. This means that cyclic creation dependencies are impossible. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.22 <0.9.0; diff --git a/docs/contracts/errors.rst b/docs/contracts/errors.rst index 3ae9a57f1..7800317a8 100644 --- a/docs/contracts/errors.rst +++ b/docs/contracts/errors.rst @@ -14,7 +14,7 @@ which causes all changes in the current call to be reverted and passes the error data back to the caller. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.4; @@ -79,4 +79,4 @@ of the built-in type ``Panic(uint256)``. of inner calls is propagated back through the chain of external calls by default. This means that an inner call can "forge" revert data that looks like it could have come from the - contract that called it. \ No newline at end of file + contract that called it. diff --git a/docs/contracts/events.rst b/docs/contracts/events.rst index e9a520e3c..aae16fb8b 100644 --- a/docs/contracts/events.rst +++ b/docs/contracts/events.rst @@ -24,9 +24,10 @@ because the contract can only see the last 256 block hashes. You can add the attribute ``indexed`` to up to three parameters which adds them to a special data structure known as :ref:`"topics" ` instead of -the data part of the log. If you use arrays (including ``string`` and ``bytes``) -as indexed arguments, its Keccak-256 hash is stored as a topic instead, this is -because a topic can only hold a single word (32 bytes). +the data part of the log. +A topic can only hold a single word (32 bytes) so if you use a :ref:`reference type +` for an indexed argument, the Keccak-256 hash of the value is stored +as a topic instead. All parameters without the ``indexed`` attribute are :ref:`ABI-encoded ` into the data part of the log. @@ -61,7 +62,16 @@ The hash of the signature of the event is one of the topics, except if you declared the event with the ``anonymous`` specifier. This means that it is not possible to filter for specific anonymous events by name, you can only filter by the contract address. The advantage of anonymous events -is that they are cheaper to deploy and call. +is that they are cheaper to deploy and call. It also allows you to declare +four indexed arguments rather than three. + +.. note:: + Since the transaction log only stores the event data and not the type, + you have to know the type of the event, including which parameter is + indexed and if the event is anonymous in order to correctly interpret + the data. + In particular, it is possible to "fake" the signature of another event + using an anonymous event. :: diff --git a/docs/contracts/function-modifiers.rst b/docs/contracts/function-modifiers.rst index 86aba6b33..262d80321 100644 --- a/docs/contracts/function-modifiers.rst +++ b/docs/contracts/function-modifiers.rst @@ -15,7 +15,7 @@ inheritable properties of contracts and may be overridden by derived contracts, if they are marked ``virtual``. For details, please see :ref:`Modifier Overriding `. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >0.7.0 <0.9.0; diff --git a/docs/contracts/functions.rst b/docs/contracts/functions.rst index 6f525a7b1..56bc85a05 100644 --- a/docs/contracts/functions.rst +++ b/docs/contracts/functions.rst @@ -12,7 +12,7 @@ Functions outside of a contract, also called "free functions", always have impli :ref:`visibility`. Their code is included in all contracts that call them, similar to internal library functions. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >0.7.0 <0.9.0; @@ -56,7 +56,9 @@ Function parameters are declared the same way as variables, and the name of unused parameters can be omitted. For example, if you want your contract to accept one kind of external call -with two integers, you would use something like the following:: +with two integers, you would use something like the following: + +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0; @@ -89,7 +91,9 @@ Function return variables are declared with the same syntax after the ``returns`` keyword. For example, suppose you want to return two results: the sum and the product of -two integers passed as function parameters, then you use something like:: +two integers passed as function parameters, then you use something like: + +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0; @@ -114,7 +118,9 @@ You can either explicitly assign to return variables and then leave the function as above, or you can provide return values (either a single or :ref:`multiple ones`) directly with the ``return`` -statement:: +statement: + +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0; @@ -178,7 +184,7 @@ The following statements are considered modifying the state: #. Using low-level calls. #. Using inline assembly that contains certain opcodes. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.5.0 <0.9.0; @@ -224,7 +230,7 @@ In addition to the list of state modifying statements explained above, the follo #. Calling any function not marked ``pure``. #. Using inline assembly that contains certain opcodes. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.5.0 <0.9.0; @@ -321,7 +327,7 @@ will consume more gas than the 2300 gas stipend: Below you can see an example of a Sink contract that uses function ``receive``. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.0 <0.9.0; @@ -383,7 +389,7 @@ operations as long as there is enough gas passed on to it. proper functions should be used instead. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.2 <0.9.0; @@ -461,7 +467,7 @@ This process is called "overloading" and also applies to inherited functions. The following example shows overloading of the function ``f`` in the scope of contract ``A``. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0; @@ -480,7 +486,7 @@ The following example shows overloading of the function Overloaded functions are also present in the external interface. It is an error if two externally visible functions differ by their Solidity types but not by their external types. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0; @@ -514,7 +520,7 @@ candidate, resolution fails. .. note:: Return parameters are not taken into account for overload resolution. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0; diff --git a/docs/contracts/inheritance.rst b/docs/contracts/inheritance.rst index 5beb226bd..24d2fd2ff 100644 --- a/docs/contracts/inheritance.rst +++ b/docs/contracts/inheritance.rst @@ -36,7 +36,7 @@ some :ref:`differences `. Details are given in the following example. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; @@ -124,7 +124,9 @@ Details are given in the following example. Note that above, we call ``Destructible.destroy()`` to "forward" the destruction request. The way this is done is problematic, as -seen in the following example:: +seen in the following example: + +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; @@ -154,7 +156,9 @@ seen in the following example:: A call to ``Final.destroy()`` will call ``Base2.destroy`` because we specify it explicitly in the final override, but this function will bypass -``Base1.destroy``. The way around this is to use ``super``:: +``Base1.destroy``. The way around this is to use ``super``: + +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; @@ -211,7 +215,7 @@ The mutability may be changed to a more strict one following the order: The following example demonstrates changing mutability and visibility: -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; @@ -235,7 +239,7 @@ and have not yet been overridden by another base contract (on some path through Additionally, if a contract inherits the same function from multiple (unrelated) bases, it has to explicitly override it: -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.0 <0.9.0; @@ -262,7 +266,7 @@ the function is defined in a common base contract or if there is a unique function in a common base contract that already overrides all other functions. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.0 <0.9.0; @@ -303,7 +307,7 @@ Public state variables can override external functions if the parameter and return types of the function matches the getter function of the variable: -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.0 <0.9.0; @@ -335,7 +339,7 @@ Function modifiers can override each other. This works in the same way as ``virtual`` keyword must be used on the overridden modifier and the ``override`` keyword must be used in the overriding modifier: -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.0 <0.9.0; @@ -354,7 +358,7 @@ and the ``override`` keyword must be used in the overriding modifier: In case of multiple inheritance, all direct base contracts must be specified explicitly: -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.0 <0.9.0; @@ -402,7 +406,7 @@ If there is no constructor, the contract will assume the default constructor, which is equivalent to ``constructor() {}``. For example: -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; @@ -439,7 +443,9 @@ Arguments for Base Constructors The constructors of all the base contracts will be called following the linearization rules explained below. If the base constructors have arguments, -derived contracts need to specify all of them. This can be done in two ways:: +derived contracts need to specify all of them. This can be done in two ways: + +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; @@ -499,7 +505,7 @@ stopping at the first match. If a base contract has already been searched, it is In the following code, Solidity will give the error "Linearization of inheritance graph impossible". -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; @@ -520,7 +526,7 @@ C3 linearization is not too important in practice. One area where inheritance linearization is especially important and perhaps not as clear is when there are multiple constructors in the inheritance hierarchy. The constructors will always be executed in the linearized order, regardless of the order in which their arguments are provided in the inheriting contract's constructor. For example: -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; diff --git a/docs/contracts/interfaces.rst b/docs/contracts/interfaces.rst index 19a33605e..bfaf1bf2e 100644 --- a/docs/contracts/interfaces.rst +++ b/docs/contracts/interfaces.rst @@ -20,7 +20,7 @@ an interface should be possible without any information loss. Interfaces are denoted by their own keyword: -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.2 <0.9.0; @@ -41,7 +41,7 @@ function is marked ``virtual``. Interfaces can inherit from other interfaces. This has the same rules as normal inheritance. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.2 <0.9.0; diff --git a/docs/contracts/libraries.rst b/docs/contracts/libraries.rst index 88e13d90c..eee9de765 100644 --- a/docs/contracts/libraries.rst +++ b/docs/contracts/libraries.rst @@ -50,7 +50,7 @@ The following example illustrates how to use libraries (but using a manual metho be sure to check out :ref:`using for ` for a more advanced example to implement a set). -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.0 <0.9.0; @@ -129,7 +129,7 @@ The following example shows how to use :ref:`types stored in memory =0.6.8 <0.9.0; @@ -238,7 +238,7 @@ The argument encoding is the same as for the regular contract ABI, except for st Similarly to the contract ABI, the selector consists of the first four bytes of the Keccak256-hash of the signature. Its value can be obtained from Solidity using the ``.selector`` member as follows: -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.5.14 <0.9.0; diff --git a/docs/contracts/using-for.rst b/docs/contracts/using-for.rst index 19cdf8e61..582409904 100644 --- a/docs/contracts/using-for.rst +++ b/docs/contracts/using-for.rst @@ -27,7 +27,9 @@ outside of the contract in which it is used. The directive may only be used inside a contract, not inside any of its functions. Let us rewrite the set example from the -:ref:`libraries` in this way:: +:ref:`libraries` in this way: + +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.0 <0.9.0; @@ -80,7 +82,9 @@ Let us rewrite the set example from the } } -It is also possible to extend elementary types in that way:: +It is also possible to extend elementary types in that way: + +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.8 <0.9.0; diff --git a/docs/contracts/visibility-and-getters.rst b/docs/contracts/visibility-and-getters.rst index d353c1911..a865f54d4 100644 --- a/docs/contracts/visibility-and-getters.rst +++ b/docs/contracts/visibility-and-getters.rst @@ -50,7 +50,7 @@ The visibility specifier is given after the type for state variables and between parameter list and return parameter list for functions. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0; @@ -65,7 +65,7 @@ In the following example, ``D``, can call ``c.getData()`` to retrieve the value ``data`` in state storage, but is not able to call ``f``. Contract ``E`` is derived from ``C`` and, thus, can call ``compute``. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0; @@ -110,7 +110,7 @@ arguments and returns a ``uint``, the value of the state variable ``data``. State variables can be initialized when they are declared. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0; @@ -131,7 +131,7 @@ symbol is accessed internally (i.e. without ``this.``), it evaluates to a state variable. If it is accessed externally (i.e. with ``this.``), it evaluates to a function. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; @@ -148,10 +148,10 @@ If you have a ``public`` state variable of array type, then you can only retriev single elements of the array via the generated getter function. This mechanism exists to avoid high gas costs when returning an entire array. You can use arguments to specify which individual element to return, for example -``data(0)``. If you want to return an entire array in one call, then you need +``myArray(0)``. If you want to return an entire array in one call, then you need to write a function, for example: -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0; @@ -178,7 +178,7 @@ Now you can use ``getArray()`` to retrieve the entire array, instead of The next example is more complex: -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; @@ -195,7 +195,7 @@ The next example is more complex: It generates a function of the following form. The mapping in the struct is omitted because there is no good way to provide the key for the mapping: -:: +.. code-block:: solidity function data(uint arg1, bool arg2, uint arg3) public returns (uint a, bytes3 b) { a = data[arg1][arg2][arg3].a; diff --git a/docs/contributing.rst b/docs/contributing.rst index 0c084c264..eaae6db3e 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -112,7 +112,7 @@ starting from the current directory. The required file is called ``libevmone.so` ``evmone.dll`` on Windows systems and ``libevmone.dylib`` on macOS. If it is not found, tests that use it are skipped. These tests are ``libsolididty/semanticTests``, ``libsolidity/GasCosts``, ``libsolidity/SolidityEndToEndTest``, part of the soltest suite. To run all tests, download the library from -`GitHub `_ +`GitHub `_ and place it in the project root path or inside the ``deps`` folder. If the ``libz3`` library is not installed on your system, you should disable the @@ -149,7 +149,7 @@ See especially: If you want to debug using GDB, make sure you build differently than the "usual". For example, you could run the following command in your ``build`` folder: -:: +.. code-block:: bash cmake -DCMAKE_BUILD_TYPE=Debug .. make @@ -171,7 +171,7 @@ The test suite compiles and checks them against the given expectations. For example: ``./test/libsolidity/syntaxTests/double_stateVariable_declaration.sol`` -:: +.. code-block:: solidity contract test { uint256 variable; @@ -190,7 +190,7 @@ In the above example, the state variable ``variable`` was declared twice, which The ``isoltest`` tool is used for these tests and you can find it under ``./build/test/tools/``. It is an interactive tool which allows editing of failing contracts using your preferred text editor. Let's try to break this test by removing the second declaration of ``variable``: -:: +.. code-block:: solidity contract test { uint256 variable; @@ -200,7 +200,7 @@ editing of failing contracts using your preferred text editor. Let's try to brea Running ``./build/test/isoltest`` again results in a test failure: -:: +.. code-block:: text syntaxTests/double_stateVariable_declaration.sol: FAIL Contract: @@ -228,7 +228,7 @@ All of these options apply to the current contract, expect ``quit`` which stops Automatically updating the test above changes it to -:: +.. code-block:: solidity contract test { uint256 variable; @@ -237,7 +237,7 @@ Automatically updating the test above changes it to and re-run the test. It now passes again: -:: +.. code-block:: text Re-running test case... syntaxTests/double_stateVariable_declaration.sol: OK @@ -263,7 +263,7 @@ We mainly use `AFL `_ for fuzzing. You need to install the AFL packages from your repositories (afl, afl-clang) or build them manually. Next, build Solidity (or just the ``solfuzzer`` binary) with AFL as your compiler: -:: +.. code-block:: bash cd build # if needed @@ -273,7 +273,7 @@ Next, build Solidity (or just the ``solfuzzer`` binary) with AFL as your compile At this stage you should be able to see a message similar to the following: -:: +.. code-block:: text Scanning dependencies of target solfuzzer [ 98%] Building CXX object test/tools/CMakeFiles/solfuzzer.dir/fuzzer.cpp.o @@ -284,7 +284,7 @@ At this stage you should be able to see a message similar to the following: If the instrumentation messages did not appear, try switching the cmake flags pointing to AFL's clang binaries: -:: +.. code-block:: bash # if previously failed make clean @@ -293,7 +293,7 @@ If the instrumentation messages did not appear, try switching the cmake flags po Otherwise, upon execution the fuzzer halts with an error saying binary is not instrumented: -:: +.. code-block:: text afl-fuzz 2.52b by ... (truncated messages) @@ -317,7 +317,7 @@ Next, you need some example source files. This makes it much easier for the fuzz to find errors. You can either copy some files from the syntax tests or extract test files from the documentation or the other tests: -:: +.. code-block:: bash mkdir /tmp/test_cases cd /tmp/test_cases @@ -334,7 +334,7 @@ that result in similar behaviour of the binary. Now run the fuzzer (the ``-m`` extends the size of memory to 60 MB): -:: +.. code-block:: bash afl-fuzz -m 60 -i /tmp/test_cases -o /tmp/fuzzer_reports -- /path/to/solfuzzer @@ -388,7 +388,8 @@ local slang and references, making your language as clear to all readers as poss .. note:: While the official Solidity documentation is written in English, there are community contributed :ref:`translations` - in other languages available. + in other languages available. Please refer to the `translation guide `_ + for information on how to contribute to the community translations. Title Case for Headings ----------------------- diff --git a/docs/control-structures.rst b/docs/control-structures.rst index f9d52148f..9da26ca26 100644 --- a/docs/control-structures.rst +++ b/docs/control-structures.rst @@ -39,11 +39,14 @@ Internal Function Calls ----------------------- Functions of the current contract can be called directly ("internally"), also recursively, as seen in -this nonsensical example:: +this nonsensical example: + +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.22 <0.9.0; + // This will report a warning contract C { function g(uint a) public pure returns (uint ret) { return a + f(); } function f() internal pure returns (uint ret) { return g(7) + f(); } @@ -81,7 +84,7 @@ Note that it is discouraged to specify gas values explicitly, since the gas cost of opcodes can change in the future. Any Wei you send to the contract is added to the total balance of that contract: -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.2 <0.9.0; @@ -143,7 +146,7 @@ if they are enclosed in ``{ }`` as can be seen in the following example. The argument list has to coincide by name with the list of parameters from the function declaration, but can be in arbitrary order. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; @@ -167,7 +170,7 @@ Omitted Function Parameter Names The names of unused parameters (especially return parameters) can be omitted. Those parameters will still be present on the stack, but they are inaccessible. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.22 <0.9.0; @@ -191,7 +194,7 @@ A contract can create other contracts using the ``new`` keyword. The full code of the contract being created has to be known when the creating contract is compiled so recursive creation-dependencies are not possible. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; @@ -246,7 +249,7 @@ contracts creates other contracts in the meantime. The main use-case here is contracts that act as judges for off-chain interactions, which only need to be created if there is a dispute. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; @@ -315,7 +318,7 @@ or to pre-existing variables (or LValues in general). Tuples are not proper types in Solidity, they can only be used to form syntactic groupings of expressions. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.5.0 <0.9.0; @@ -361,7 +364,7 @@ In the example below the call to ``g(x)`` has no effect on ``x`` because it crea an independent copy of the storage value in memory. However, ``h(x)`` successfully modifies ``x`` because only a reference and not a copy is passed. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.22 <0.9.0; @@ -420,7 +423,7 @@ use state variables before they are declared and call functions recursively. As a consequence, the following examples will compile without warnings, since the two variables have the same name but disjoint scopes. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.5.0 <0.9.0; @@ -442,7 +445,7 @@ As a special example of the C99 scoping rules, note that in the following, the first assignment to ``x`` will actually assign the outer and not the inner variable. In any case, you will get a warning about the outer variable being shadowed. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.5.0 <0.9.0; @@ -464,7 +467,7 @@ In any case, you will get a warning about the outer variable being shadowed. for the entire function, regardless where it was declared. The following example shows a code snippet that used to compile but leads to an error starting from version 0.5.0. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.5.0 <0.9.0; @@ -496,7 +499,7 @@ thus making the use of these libraries unnecessary. To obtain the previous behaviour, an ``unchecked`` block can be used: -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.0; @@ -598,6 +601,7 @@ function calls which will cause a Panic. A Panic exception is generated in the following situations. The error code supplied with the error data indicates the kind of panic. +#. 0x00: Used for generic compiler inserted panics. #. 0x01: If you call ``assert`` with an argument that evaluates to false. #. 0x11: If an arithmetic operation results in underflow or overflow outside of an ``unchecked { ... }`` block. #. 0x12; If you divide or modulo by zero (e.g. ``5 / 0`` or ``23 % 0``). @@ -655,7 +659,7 @@ You can optionally provide a message string for ``require``, but not for ``asser The following example shows how you can use ``require`` to check conditions on inputs and ``assert`` for internal error checking. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.5.0 <0.9.0; @@ -719,7 +723,7 @@ any costs. The following example shows how to use an error string and a custom error instance together with ``revert`` and the equivalent ``require``: -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.4; @@ -779,7 +783,7 @@ The provided message can be retrieved by the caller using ``try``/``catch`` as s A failure in an external call can be caught using a try/catch statement, as follows: -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >0.8.0; diff --git a/docs/examples/blind-auction.rst b/docs/examples/blind-auction.rst index 802b4d171..ce72cdf18 100644 --- a/docs/examples/blind-auction.rst +++ b/docs/examples/blind-auction.rst @@ -22,7 +22,7 @@ raised, the previous highest bidder gets their money back. After the end of the bidding period, the contract has to be called manually for the beneficiary to receive their money - contracts cannot activate themselves. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.4; @@ -191,7 +191,7 @@ transfers): Bidders can confuse competition by placing several high or low invalid bids. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.4; diff --git a/docs/examples/micropayment.rst b/docs/examples/micropayment.rst index 09f5324b6..b9030747b 100644 --- a/docs/examples/micropayment.rst +++ b/docs/examples/micropayment.rst @@ -40,7 +40,7 @@ using `web3.js `_ and `MetaMask `_, using the method described in `EIP-762 `_, as it provides a number of other security benefits. -:: +.. code-block:: javascript /// Hashing first makes things easier var hash = web3.utils.sha3("message to sign"); @@ -90,7 +90,7 @@ library provides a function called ``soliditySHA3`` that mimics the behaviour of Solidity's ``keccak256`` function applied to arguments encoded using ``abi.encodePacked``. Here is a JavaScript function that creates the proper signature for the ``ReceiverPays`` example: -:: +.. code-block:: javascript // recipient is the address that should be paid. // amount, in wei, specifies how much ether should be sent. @@ -139,7 +139,8 @@ The functions ``prefixed`` and ``recoverSigner`` do this in the ``claimPayment`` The full contract ----------------- -:: +.. code-block:: solidity + :force: // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; @@ -271,7 +272,7 @@ to prevent a message intended for one payment channel from being used for a diff Here is the modified JavaScript code to cryptographically sign a message from the previous section: -:: +.. code-block:: javascript function constructPaymentMessage(contractAddress, amount) { return abi.soliditySHA3( @@ -335,7 +336,8 @@ so it is important that Bob closes the channel before the expiration is reached. The full contract ----------------- -:: +.. code-block:: solidity + :force: // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; @@ -452,9 +454,9 @@ The recipient should verify each message using the following process: We'll use the `ethereumjs-util `_ library to write this verification. The final step can be done a number of ways, -and we use JavaScript. The following code borrows the ``constructMessage`` function from the signing **JavaScript code** above: +and we use JavaScript. The following code borrows the ``constructPaymentMessage`` function from the signing **JavaScript code** above: -:: +.. code-block:: javascript // this mimics the prefixing behavior of the eth_sign JSON-RPC method. function prefixed(hash) { diff --git a/docs/examples/modular.rst b/docs/examples/modular.rst index 5ad6a691d..d91a744d4 100644 --- a/docs/examples/modular.rst +++ b/docs/examples/modular.rst @@ -17,7 +17,7 @@ provides an isolated component that properly tracks balances of accounts. It is easy to verify that the ``Balances`` library never produces negative balances or overflows and the sum of all balances is an invariant across the lifetime of the contract. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.5.0 <0.9.0; diff --git a/docs/examples/safe-remote.rst b/docs/examples/safe-remote.rst index 5a24e5a8f..4c6dab1be 100644 --- a/docs/examples/safe-remote.rst +++ b/docs/examples/safe-remote.rst @@ -23,7 +23,7 @@ This contract of course does not solve the problem, but gives an overview of how you can use state machine-like constructs inside a contract. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.4; diff --git a/docs/examples/voting.rst b/docs/examples/voting.rst index 299816fd9..2c229041f 100644 --- a/docs/examples/voting.rst +++ b/docs/examples/voting.rst @@ -30,7 +30,7 @@ At the end of the voting time, ``winningProposal()`` will return the proposal with the largest number of votes. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; diff --git a/docs/index.rst b/docs/index.rst index cb9fd6479..44ddbff83 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -91,6 +91,12 @@ Community volunteers help translate this documentation into several languages. They have varying degrees of completeness and up-to-dateness. The English version stands as a reference. +.. note:: + + We recently set up a new GitHub organization and translation workflow to help streamline the + community efforts. Please refer to the `translation guide `_ + for information on how to contribute to the community translations moving forward. + * `French `_ (in progress) * `Italian `_ (in progress) * `Japanese `_ @@ -143,7 +149,7 @@ Contents internals/layout_in_calldata.rst internals/variable_cleanup.rst internals/source_mappings.rst - internals/optimiser.rst + internals/optimizer.rst metadata.rst abi-spec.rst @@ -160,6 +166,7 @@ Contents security-considerations.rst smtchecker.rst resources.rst + path-resolution.rst yul.rst style-guide.rst common-patterns.rst diff --git a/docs/installing-solidity.rst b/docs/installing-solidity.rst index 7c676ff90..a0857e038 100644 --- a/docs/installing-solidity.rst +++ b/docs/installing-solidity.rst @@ -86,7 +86,9 @@ local folder for input and output, and specify the contract to compile. For exam docker run -v /local/path:/sources ethereum/solc:stable -o /sources/output --abi --bin /sources/Contract.sol You can also use the standard JSON interface (which is recommended when using the compiler with tooling). -When using this interface it is not necessary to mount any directories. +When using this interface it is not necessary to mount any directories as long as the JSON input is +self-contained (i.e. it does not refer to any external files that would have to be +:ref:`loaded by the import callback `). .. code-block:: bash diff --git a/docs/internals/layout_in_memory.rst b/docs/internals/layout_in_memory.rst index bad8c1654..66623572f 100644 --- a/docs/internals/layout_in_memory.rst +++ b/docs/internals/layout_in_memory.rst @@ -49,7 +49,7 @@ Example for Difference in Arrays The following array occupies 32 bytes (1 slot) in storage, but 128 bytes (4 items with 32 bytes each) in memory. -:: +.. code-block:: solidity uint8[4] a; @@ -62,7 +62,7 @@ The following struct occupies 96 bytes (3 slots of 32 bytes) in storage, but 128 bytes (4 items with 32 bytes each) in memory. -:: +.. code-block:: solidity struct S { uint a; diff --git a/docs/internals/layout_in_storage.rst b/docs/internals/layout_in_storage.rst index 68373dbd4..a9de8f6c9 100644 --- a/docs/internals/layout_in_storage.rst +++ b/docs/internals/layout_in_storage.rst @@ -98,7 +98,7 @@ for example, you have to add an offset corresponding to the struct member to rea As an example, consider the following contract: -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; @@ -216,7 +216,7 @@ The following example shows a contract and its storage layout, containing value and reference types, types that are encoded packed, and nested types. -.. code:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; @@ -238,12 +238,12 @@ value and reference types, types that are encoded packed, and nested types. bytes b1; } -.. code:: +.. code:: json - "storageLayout": { + { "storage": [ { - "astId": 14, + "astId": 15, "contract": "fileA:A", "label": "x", "offset": 0, @@ -251,7 +251,7 @@ value and reference types, types that are encoded packed, and nested types. "type": "t_uint256" }, { - "astId": 16, + "astId": 17, "contract": "fileA:A", "label": "y", "offset": 0, @@ -259,15 +259,15 @@ value and reference types, types that are encoded packed, and nested types. "type": "t_uint256" }, { - "astId": 18, + "astId": 20, "contract": "fileA:A", "label": "s", "offset": 0, "slot": "2", - "type": "t_struct(S)12_storage" + "type": "t_struct(S)13_storage" }, { - "astId": 20, + "astId": 22, "contract": "fileA:A", "label": "addr", "offset": 0, @@ -275,7 +275,7 @@ value and reference types, types that are encoded packed, and nested types. "type": "t_address" }, { - "astId": 26, + "astId": 28, "contract": "fileA:A", "label": "map", "offset": 0, @@ -283,7 +283,7 @@ value and reference types, types that are encoded packed, and nested types. "type": "t_mapping(t_uint256,t_mapping(t_address,t_bool))" }, { - "astId": 29, + "astId": 31, "contract": "fileA:A", "label": "array", "offset": 0, @@ -291,7 +291,7 @@ value and reference types, types that are encoded packed, and nested types. "type": "t_array(t_uint256)dyn_storage" }, { - "astId": 31, + "astId": 33, "contract": "fileA:A", "label": "s1", "offset": 0, @@ -299,7 +299,7 @@ value and reference types, types that are encoded packed, and nested types. "type": "t_string_storage" }, { - "astId": 33, + "astId": 35, "contract": "fileA:A", "label": "b1", "offset": 0, @@ -354,12 +354,12 @@ value and reference types, types that are encoded packed, and nested types. "label": "string", "numberOfBytes": "32" }, - "t_struct(S)12_storage": { + "t_struct(S)13_storage": { "encoding": "inplace", "label": "struct A.S", "members": [ { - "astId": 2, + "astId": 3, "contract": "fileA:A", "label": "a", "offset": 0, @@ -367,7 +367,7 @@ value and reference types, types that are encoded packed, and nested types. "type": "t_uint128" }, { - "astId": 4, + "astId": 5, "contract": "fileA:A", "label": "b", "offset": 16, @@ -375,7 +375,7 @@ value and reference types, types that are encoded packed, and nested types. "type": "t_uint128" }, { - "astId": 8, + "astId": 9, "contract": "fileA:A", "label": "staticArray", "offset": 0, @@ -383,7 +383,7 @@ value and reference types, types that are encoded packed, and nested types. "type": "t_array(t_uint256)2_storage" }, { - "astId": 11, + "astId": 12, "contract": "fileA:A", "label": "dynArray", "offset": 0, diff --git a/docs/internals/optimiser.rst b/docs/internals/optimiser.rst deleted file mode 100644 index edf5c4c70..000000000 --- a/docs/internals/optimiser.rst +++ /dev/null @@ -1,141 +0,0 @@ -.. index:: optimizer, common subexpression elimination, constant propagation - -************* -The Optimiser -************* - -This section discusses the optimiser that was first added to Solidity, -which operates on opcode streams. For information on the new Yul-based optimiser, -please see the `readme on github `_. - -The Solidity optimiser operates on assembly. It splits the sequence of instructions into basic blocks -at ``JUMPs`` and ``JUMPDESTs``. Inside these blocks, the optimiser -analyses the instructions and records every modification to the stack, -memory, or storage as an expression which consists of an instruction and -a list of arguments which are pointers to other expressions. The optimiser -uses a component called "CommonSubexpressionEliminator" that amongst other -tasks, finds expressions that are always equal (on every input) and combines -them into an expression class. The optimiser first tries to find each new -expression in a list of already known expressions. If this does not work, -it simplifies the expression according to rules like -``constant + constant = sum_of_constants`` or ``X * 1 = X``. Since this is -a recursive process, we can also apply the latter rule if the second factor -is a more complex expression where we know that it always evaluates to one. -Modifications to storage and memory locations have to erase knowledge about -storage and memory locations which are not known to be different. If we first -write to location x and then to location y and both are input variables, the -second could overwrite the first, so we do not know what is stored at x after -we wrote to y. If simplification of the expression x - y evaluates to a -non-zero constant, we know that we can keep our knowledge about what is stored at x. - -After this process, we know which expressions have to be on the stack at -the end, and have a list of modifications to memory and storage. This information -is stored together with the basic blocks and is used to link them. Furthermore, -knowledge about the stack, storage and memory configuration is forwarded to -the next block(s). If we know the targets of all ``JUMP`` and ``JUMPI`` instructions, -we can build a complete control flow graph of the program. If there is only -one target we do not know (this can happen as in principle, jump targets can -be computed from inputs), we have to erase all knowledge about the input state -of a block as it can be the target of the unknown ``JUMP``. If the optimiser -finds a ``JUMPI`` whose condition evaluates to a constant, it transforms it -to an unconditional jump. - -As the last step, the code in each block is re-generated. The optimiser creates -a dependency graph from the expressions on the stack at the end of the block, -and it drops every operation that is not part of this graph. It generates code -that applies the modifications to memory and storage in the order they were -made in the original code (dropping modifications which were found not to be -needed). Finally, it generates all values that are required to be on the -stack in the correct place. - -These steps are applied to each basic block and the newly generated code -is used as replacement if it is smaller. If a basic block is split at a -``JUMPI`` and during the analysis, the condition evaluates to a constant, -the ``JUMPI`` is replaced depending on the value of the constant. Thus code like - -:: - - uint x = 7; - data[7] = 9; - if (data[x] != x + 2) - return 2; - else - return 1; - -still simplifies to code which you can compile even though the instructions contained -a jump in the beginning of the process: - -:: - - data[7] = 9; - return 1; - -Simple Inlining ---------------- - -Since Solidity version 0.8.2, there is another optimizer step that replaces certain -jumps to blocks containing "simple" instructions ending with a "jump" by a copy of these instructions. -This corresponds to inlining of simple, small Solidity or Yul functions. In particular, the sequence -``PUSHTAG(tag) JUMP`` may be replaced, whenever the ``JUMP`` is marked as jump "into" a -function and behind ``tag`` there is a basic block (as described above for the -"CommonSubexpressionEliminator") that ends in another ``JUMP`` which is marked as a jump -"out of" a function. -In particular, consider the following prototypical example of assembly generated for a -call to an internal Solidity function: - -.. code-block:: text - - tag_return - tag_f - jump // in - tag_return: - ...opcodes after call to f... - - tag_f: - ...body of function f... - jump // out - -As long as the body of the function is a continuous basic block, the "Inliner" can replace ``tag_f jump`` by -the block at ``tag_f`` resulting in: - -.. code-block:: text - - tag_return - ...body of function f... - jump - tag_return: - ...opcodes after call to f... - - tag_f: - ...body of function f... - jump // out - -Now ideally, the other optimiser steps described above will result in the return tag push being moved -towards the remaining jump resulting in: - -.. code-block:: text - - ...body of function f... - tag_return - jump - tag_return: - ...opcodes after call to f... - - tag_f: - ...body of function f... - jump // out - -In this situation the "PeepholeOptimizer" will remove the return jump. Ideally, all of this can be done -for all references to ``tag_f`` leaving it unused, s.t. it can be removed, yielding: - -.. code-block:: text - - ...body of function f... - ...opcodes after call to f... - -So the call to function ``f`` is inlined and the original definition of ``f`` can be removed. - -Inlining like this is attempted, whenever a heuristics suggests that inlining is cheaper over the lifetime of a -contract than not inlining. This heuristics depends on the size of the function body, the -number of other references to its tag (approximating the number of calls to the function) and -the expected number of executions of the contract (the global optimiser parameter "runs"). diff --git a/docs/internals/optimizer.rst b/docs/internals/optimizer.rst new file mode 100644 index 000000000..c1107d1b4 --- /dev/null +++ b/docs/internals/optimizer.rst @@ -0,0 +1,1306 @@ +.. index:: optimizer, optimiser, common subexpression elimination, constant propagation +.. _optimizer: + +************* +The Optimizer +************* + +The Solidity compiler uses two different optimizer modules: The "old" optimizer +that operates at the opcode level and the "new" optimizer that operates on Yul IR code. + +The opcode-based optimizer applies a set of `simplification rules `_ +to opcodes. It also combines equal code sets and removes unused code. + +The Yul-based optimizer is much more powerful, because it can work across function +calls. For example, arbitrary jumps are not possible in Yul, so it is +possible to compute the side-effects of each function. Consider two function calls, +where the first does not modify storage and the second does modify storage. +If their arguments and return values do not depend on each other, we can reorder +the function calls. Similarly, if a function is +side-effect free and its result is multiplied by zero, you can remove the function +call completely. + +Currently, the parameter ``--optimize`` activates the opcode-based optimizer for the +generated bytecode and the Yul optimizer for the Yul code generated internally, for example for ABI coder v2. +One can use ``solc --ir-optimized --optimize`` to produce an +optimized experimental Yul IR for a Solidity source. Similarly, one can use ``solc --strict-assembly --optimize`` +for a stand-alone Yul mode. + +You can find more details on both optimizer modules and their optimization steps below. + +Benefits of Optimizing Solidity Code +==================================== + +Overall, the optimizer tries to simplify complicated expressions, which reduces both code +size and execution cost, i.e., it can reduce gas needed for contract deployment as well as for external calls made to the contract. +It also specializes or inlines functions. Especially +function inlining is an operation that can cause much bigger code, but it is +often done because it results in opportunities for more simplifications. + + +Differences between Optimized and Non-Optimized Code +==================================================== + +Generally, the most visible difference is that constant expressions are evaluated at compile time. +When it comes to the ASM output, one can also notice a reduction of equivalent or duplicate +code blocks (compare the output of the flags ``--asm`` and ``--asm --optimize``). However, +when it comes to the Yul/intermediate-representation, there can be significant +differences, for example, functions may be inlined, combined, or rewritten to eliminate +redundancies, etc. (compare the output between the flags ``--ir`` and +``--optimize --ir-optimized``). + +Optimizer Parameter Runs +======================== + +The number of runs (``--optimize-runs``) specifies roughly how often each opcode of the +deployed code will be executed across the life-time of the contract. This means it is a +trade-off parameter between code size (deploy cost) and code execution cost (cost after deployment). +A "runs" parameter of "1" will produce short but expensive code. In contrast, a larger "runs" +parameter will produce longer but more gas efficient code. The maximum value of the parameter +is ``2**32-1``. + +.. note:: + + A common misconception is that this parameter specifies the number of iterations of the optimizer. + This is not true: The optimizer will always run as many times as it can still improve the code. + +Opcode-Based Optimizer Module +============================= + +The opcode-based optimizer module operates on assembly code. It splits the +sequence of instructions into basic blocks at ``JUMPs`` and ``JUMPDESTs``. +Inside these blocks, the optimizer analyzes the instructions and records every modification to the stack, +memory, or storage as an expression which consists of an instruction and +a list of arguments which are pointers to other expressions. + +Additionally, the opcode-based optimizer +uses a component called "CommonSubexpressionEliminator" that, amongst other +tasks, finds expressions that are always equal (on every input) and combines +them into an expression class. It first tries to find each new +expression in a list of already known expressions. If no such matches are found, +it simplifies the expression according to rules like +``constant + constant = sum_of_constants`` or ``X * 1 = X``. Since this is +a recursive process, we can also apply the latter rule if the second factor +is a more complex expression which we know always evaluates to one. + +Certain optimizer steps symbolically track the storage and memory locations. For example, this +information is used to compute Keccak-256 hashes that can be evaluated during compile time. Consider +the sequence: + +.. code-block:: none + + PUSH 32 + PUSH 0 + CALLDATALOAD + PUSH 100 + DUP2 + MSTORE + KECCAK256 + +or the equivalent Yul + +.. code-block:: yul + + let x := calldataload(0) + mstore(x, 100) + let value := keccak256(x, 32) + +In this case, the optimizer tracks the value at a memory location ``calldataload(0)`` and then +realizes that the Keccak-256 hash can be evaluated at compile time. This only works if there is no +other instruction that modifies memory between the ``mstore`` and ``keccak256``. So if there is an +instruction that writes to memory (or storage), then we need to erase the knowledge of the current +memory (or storage). There is, however, an exception to this erasing, when we can easily see that +the instruction doesn't write to a certain location. + +For example, + +.. code-block:: yul + + let x := calldataload(0) + mstore(x, 100) + // Current knowledge memory location x -> 100 + let y := add(x, 32) + // Does not clear the knowledge that x -> 100, since y does not write to [x, x + 32) + mstore(y, 200) + // This Keccak-256 can now be evaluated + let value := keccak256(x, 32) + +Therefore, modifications to storage and memory locations, of say location ``l``, must erase +knowledge about storage or memory locations which may be equal to ``l``. More specifically, for +storage, the optimizer has to erase all knowledge of symbolic locations, that may be equal to ``l`` +and for memory, the optimizer has to erase all knowledge of symbolic locations that may not be at +least 32 bytes away. If ``m`` denotes an arbitrary location, then this decision on erasure is done +by computing the value ``sub(l, m)``. For storage, if this value evaluates to a literal that is +non-zero, then the knowledge about ``m`` will be kept. For memory, if the value evaluates to a +literal that is between ``32`` and ``2**256 - 32``, then the knowledge about ``m`` will be kept. In +all other cases, the knowledge about ``m`` will be erased. + +After this process, we know which expressions have to be on the stack at +the end, and have a list of modifications to memory and storage. This information +is stored together with the basic blocks and is used to link them. Furthermore, +knowledge about the stack, storage and memory configuration is forwarded to +the next block(s). + +If we know the targets of all ``JUMP`` and ``JUMPI`` instructions, +we can build a complete control flow graph of the program. If there is only +one target we do not know (this can happen as in principle, jump targets can +be computed from inputs), we have to erase all knowledge about the input state +of a block as it can be the target of the unknown ``JUMP``. If the opcode-based +optimizer module finds a ``JUMPI`` whose condition evaluates to a constant, it transforms it +to an unconditional jump. + +As the last step, the code in each block is re-generated. The optimizer creates +a dependency graph from the expressions on the stack at the end of the block, +and it drops every operation that is not part of this graph. It generates code +that applies the modifications to memory and storage in the order they were +made in the original code (dropping modifications which were found not to be +needed). Finally, it generates all values that are required to be on the +stack in the correct place. + +These steps are applied to each basic block and the newly generated code +is used as replacement if it is smaller. If a basic block is split at a +``JUMPI`` and during the analysis, the condition evaluates to a constant, +the ``JUMPI`` is replaced based on the value of the constant. Thus code like + +.. code-block:: solidity + + uint x = 7; + data[7] = 9; + if (data[x] != x + 2) // this condition is never true + return 2; + else + return 1; + +simplifies to this: + +.. code-block:: solidity + + data[7] = 9; + return 1; + +Simple Inlining +--------------- + +Since Solidity version 0.8.2, there is another optimizer step that replaces certain +jumps to blocks containing "simple" instructions ending with a "jump" by a copy of these instructions. +This corresponds to inlining of simple, small Solidity or Yul functions. In particular, the sequence +``PUSHTAG(tag) JUMP`` may be replaced, whenever the ``JUMP`` is marked as jump "into" a +function and behind ``tag`` there is a basic block (as described above for the +"CommonSubexpressionEliminator") that ends in another ``JUMP`` which is marked as a jump +"out of" a function. + +In particular, consider the following prototypical example of assembly generated for a +call to an internal Solidity function: + +.. code-block:: text + + tag_return + tag_f + jump // in + tag_return: + ...opcodes after call to f... + + tag_f: + ...body of function f... + jump // out + +As long as the body of the function is a continuous basic block, the "Inliner" can replace ``tag_f jump`` by +the block at ``tag_f`` resulting in: + +.. code-block:: text + + tag_return + ...body of function f... + jump + tag_return: + ...opcodes after call to f... + + tag_f: + ...body of function f... + jump // out + +Now ideally, the other optimizer steps described above will result in the return tag push being moved +towards the remaining jump resulting in: + +.. code-block:: text + + ...body of function f... + tag_return + jump + tag_return: + ...opcodes after call to f... + + tag_f: + ...body of function f... + jump // out + +In this situation the "PeepholeOptimizer" will remove the return jump. Ideally, all of this can be done +for all references to ``tag_f`` leaving it unused, s.t. it can be removed, yielding: + +.. code-block:: text + + ...body of function f... + ...opcodes after call to f... + +So the call to function ``f`` is inlined and the original definition of ``f`` can be removed. + +Inlining like this is attempted, whenever a heuristics suggests that inlining is cheaper over the lifetime of a +contract than not inlining. This heuristics depends on the size of the function body, the +number of other references to its tag (approximating the number of calls to the function) and +the expected number of executions of the contract (the global optimizer parameter "runs"). + + +Yul-Based Optimizer Module +========================== + +The Yul-based optimizer consists of several stages and components that all transform +the AST in a semantically equivalent way. The goal is to end up either with code +that is shorter or at least only marginally longer but will allow further +optimization steps. + +.. warning:: + + Since the optimizer is under heavy development, the information here might be outdated. + If you rely on a certain functionality, please reach out to the team directly. + +The optimizer currently follows a purely greedy strategy and does not do any +backtracking. + +All components of the Yul-based optimizer module are explained below. +The following transformation steps are the main components: + + - SSA Transform + - Common Subexpression Eliminator + - Expression Simplifier + - Redundant Assign Eliminator + - Full Function Inliner + +Optimizer Steps +--------------- + +This is a list of all steps the Yul-based optimizer sorted alphabetically. You can find more information +on the individual steps and their sequence below. + + - :ref:`block-flattener`. + - :ref:`circular-reference-pruner`. + - :ref:`common-subexpression-eliminator`. + - :ref:`conditional-simplifier`. + - :ref:`conditional-unsimplifier`. + - :ref:`control-flow-simplifier`. + - :ref:`dead-code-eliminator`. + - :ref:`equivalent-function-combiner`. + - :ref:`expression-joiner`. + - :ref:`expression-simplifier`. + - :ref:`expression-splitter`. + - :ref:`for-loop-condition-into-body`. + - :ref:`for-loop-condition-out-of-body`. + - :ref:`for-loop-init-rewriter`. + - :ref:`functional-inliner`. + - :ref:`function-grouper`. + - :ref:`function-hoister`. + - :ref:`function-specializer`. + - :ref:`literal-rematerialiser`. + - :ref:`load-resolver`. + - :ref:`loop-invariant-code-motion`. + - :ref:`redundant-assign-eliminator`. + - :ref:`reasoning-based-simplifier`. + - :ref:`rematerialiser`. + - :ref:`SSA-reverser`. + - :ref:`SSA-transform`. + - :ref:`structural-simplifier`. + - :ref:`unused-function-parameter-pruner`. + - :ref:`unused-pruner`. + - :ref:`var-decl-initializer`. + +Selecting Optimizations +----------------------- + +By default the optimizer applies its predefined sequence of optimization steps to +the generated assembly. You can override this sequence and supply your own using +the ``--yul-optimizations`` option: + +.. code-block:: text + + bash + solc --optimize --ir-optimized --yul-optimizations 'dhfoD[xarrscLMcCTU]uljmul' + +The sequence inside ``[...]`` will be applied multiple times in a loop until the Yul code +remains unchanged or until the maximum number of rounds (currently 12) has been reached. + +Available abbreviations are listed in the `Yul optimizer docs `_. + +Preprocessing +------------- + +The preprocessing components perform transformations to get the program +into a certain normal form that is easier to work with. This normal +form is kept during the rest of the optimization process. + +.. _disambiguator: + +Disambiguator +^^^^^^^^^^^^^ + +The disambiguator takes an AST and returns a fresh copy where all identifiers have +unique names in the input AST. This is a prerequisite for all other optimizer stages. +One of the benefits is that identifier lookup does not need to take scopes into account +which simplifies the analysis needed for other steps. + +All subsequent stages have the property that all names stay unique. This means if +a new identifier needs to be introduced, a new unique name is generated. + +.. _function-hoister: + +FunctionHoister +^^^^^^^^^^^^^^^ + +The function hoister moves all function definitions to the end of the topmost block. This is +a semantically equivalent transformation as long as it is performed after the +disambiguation stage. The reason is that moving a definition to a higher-level block cannot decrease +its visibility and it is impossible to reference variables defined in a different function. + +The benefit of this stage is that function definitions can be looked up more easily +and functions can be optimized in isolation without having to traverse the AST completely. + +.. _function-grouper: + +FunctionGrouper +^^^^^^^^^^^^^^^ + +The function grouper has to be applied after the disambiguator and the function hoister. +Its effect is that all topmost elements that are not function definitions are moved +into a single block which is the first statement of the root block. + +After this step, a program has the following normal form: + +.. code-block:: text + + { I F... } + +Where ``I`` is a (potentially empty) block that does not contain any function definitions (not even recursively) +and ``F`` is a list of function definitions such that no function contains a function definition. + +The benefit of this stage is that we always know where the list of function begins. + +.. _for-loop-condition-into-body: + +ForLoopConditionIntoBody +^^^^^^^^^^^^^^^^^^^^^^^^ + +This transformation moves the loop-iteration condition of a for-loop into loop body. +We need this transformation because :ref:`expression-splitter` will not +apply to iteration condition expressions (the ``C`` in the following example). + +.. code-block:: text + + for { Init... } C { Post... } { + Body... + } + +is transformed to + +.. code-block:: text + + for { Init... } 1 { Post... } { + if iszero(C) { break } + Body... + } + +This transformation can also be useful when paired with ``LoopInvariantCodeMotion``, since +invariants in the loop-invariant conditions can then be taken outside the loop. + +.. _for-loop-init-rewriter: + +ForLoopInitRewriter +^^^^^^^^^^^^^^^^^^^ + +This transformation moves the initialization part of a for-loop to before +the loop: + +.. code-block:: text + + for { Init... } C { Post... } { + Body... + } + +is transformed to + +.. code-block:: text + + { + Init... + for {} C { Post... } { + Body... + } + } + +This eases the rest of the optimization process because we can ignore +the complicated scoping rules of the for loop initialisation block. + +.. _var-decl-initializer: + +VarDeclInitializer +^^^^^^^^^^^^^^^^^^ +This step rewrites variable declarations so that all of them are initialized. +Declarations like ``let x, y`` are split into multiple declaration statements. + +Only supports initializing with the zero literal for now. + +Pseudo-SSA Transformation +------------------------- + +The purpose of this components is to get the program into a longer form, +so that other components can more easily work with it. The final representation +will be similar to a static-single-assignment (SSA) form, with the difference +that it does not make use of explicit "phi" functions which combines the values +from different branches of control flow because such a feature does not exist +in the Yul language. Instead, when control flow merges, if a variable is re-assigned +in one of the branches, a new SSA variable is declared to hold its current value, +so that the following expressions still only need to reference SSA variables. + +An example transformation is the following: + +.. code-block:: yul + + { + let a := calldataload(0) + let b := calldataload(0x20) + if gt(a, 0) { + b := mul(b, 0x20) + } + a := add(a, 1) + sstore(a, add(b, 0x20)) + } + + +When all the following transformation steps are applied, the program will look +as follows: + +.. code-block:: yul + + { + let _1 := 0 + let a_9 := calldataload(_1) + let a := a_9 + let _2 := 0x20 + let b_10 := calldataload(_2) + let b := b_10 + let _3 := 0 + let _4 := gt(a_9, _3) + if _4 + { + let _5 := 0x20 + let b_11 := mul(b_10, _5) + b := b_11 + } + let b_12 := b + let _6 := 1 + let a_13 := add(a_9, _6) + let _7 := 0x20 + let _8 := add(b_12, _7) + sstore(a_13, _8) + } + +Note that the only variable that is re-assigned in this snippet is ``b``. +This re-assignment cannot be avoided because ``b`` has different values +depending on the control flow. All other variables never change their +value once they are defined. The advantage of this property is that +variables can be freely moved around and references to them +can be exchanged by their initial value (and vice-versa), +as long as these values are still valid in the new context. + +Of course, the code here is far from being optimized. To the contrary, it is much +longer. The hope is that this code will be easier to work with and furthermore, +there are optimizer steps that undo these changes and make the code more +compact again at the end. + +.. _expression-splitter: + +ExpressionSplitter +^^^^^^^^^^^^^^^^^^ + +The expression splitter turns expressions like ``add(mload(x), mul(mload(y), 0x20))`` +into a sequence of declarations of unique variables that are assigned sub-expressions +of that expression so that each function call has only variables or literals +as arguments. + +The above would be transformed into + +.. code-block:: yul + + { + let _1 := mload(y) + let _2 := mul(_1, 0x20) + let _3 := mload(x) + let z := add(_3, _2) + } + +Note that this transformation does not change the order of opcodes or function calls. + +It is not applied to loop iteration-condition, because the loop control flow does not allow +this "outlining" of the inner expressions in all cases. We can sidestep this limitation by applying +:ref:`for-loop-condition-into-body` to move the iteration condition into loop body. + +The final program should be in a form such that (with the exception of loop conditions) +function calls cannot appear nested inside expressions +and all function call arguments have to be literals or variables. + +The benefits of this form are that it is much easier to re-order the sequence of opcodes +and it is also easier to perform function call inlining. Furthermore, it is simpler +to replace individual parts of expressions or re-organize the "expression tree". +The drawback is that such code is much harder to read for humans. + +.. _SSA-transform: + +SSATransform +^^^^^^^^^^^^ + +This stage tries to replace repeated assignments to +existing variables by declarations of new variables as much as +possible. +The reassignments are still there, but all references to the +reassigned variables are replaced by the newly declared variables. + +Example: + +.. code-block:: yul + + { + let a := 1 + mstore(a, 2) + a := 3 + } + +is transformed to + +.. code-block:: yul + + { + let a_1 := 1 + let a := a_1 + mstore(a_1, 2) + let a_3 := 3 + a := a_3 + } + +Exact semantics: + +For any variable ``a`` that is assigned to somewhere in the code +(variables that are declared with value and never re-assigned +are not modified) perform the following transforms: + + - replace ``let a := v`` by ``let a_i := v let a := a_i`` + - replace ``a := v`` by ``let a_i := v a := a_i`` where ``i`` is a number such that ``a_i`` is yet unused. + +Furthermore, always record the current value of ``i`` used for ``a`` and replace each +reference to ``a`` by ``a_i``. +The current value mapping is cleared for a variable ``a`` at the end of each block +in which it was assigned to and at the end of the for loop init block if it is assigned +inside the for loop body or post block. +If a variable's value is cleared according to the rule above and the variable is declared outside +the block, a new SSA variable will be created at the location where control flow joins, +this includes the beginning of loop post/body block and the location right after +If/Switch/ForLoop/Block statement. + +After this stage, the Redundant Assign Eliminator is recommended to remove the unnecessary +intermediate assignments. + +This stage provides best results if the Expression Splitter and the Common Subexpression Eliminator +are run right before it, because then it does not generate excessive amounts of variables. +On the other hand, the Common Subexpression Eliminator could be more efficient if run after the +SSA transform. + +.. _redundant-assign-eliminator: + +RedundantAssignEliminator +^^^^^^^^^^^^^^^^^^^^^^^^^ + +The SSA transform always generates an assignment of the form ``a := a_i``, even though +these might be unnecessary in many cases, like the following example: + +.. code-block:: yul + + { + let a := 1 + a := mload(a) + a := sload(a) + sstore(a, 1) + } + +The SSA transform converts this snippet to the following: + +.. code-block:: yul + + { + let a_1 := 1 + a := a_1 + let a_2 := mload(a_1) + a := a_2 + let a_3 := sload(a_2) + a := a_3 + sstore(a_3, 1) + } + +The Redundant Assign Eliminator removes all the three assignments to ``a``, because +the value of ``a`` is not used and thus turn this +snippet into strict SSA form: + +.. code-block:: yul + + { + let a_1 := 1 + let a_2 := mload(a_1) + let a_3 := sload(a_2) + sstore(a_3, 1) + } + +Of course the intricate parts of determining whether an assignment is redundant or not +are connected to joining control flow. + +The component works as follows in detail: + +The AST is traversed twice: in an information gathering step and in the +actual removal step. During information gathering, we maintain a +mapping from assignment statements to the three states +"unused", "undecided" and "used" which signifies whether the assigned +value will be used later by a reference to the variable. + +When an assignment is visited, it is added to the mapping in the "undecided" state +(see remark about for loops below) and every other assignment to the same variable +that is still in the "undecided" state is changed to "unused". +When a variable is referenced, the state of any assignment to that variable still +in the "undecided" state is changed to "used". + +At points where control flow splits, a copy +of the mapping is handed over to each branch. At points where control flow +joins, the two mappings coming from the two branches are combined in the following way: +Statements that are only in one mapping or have the same state are used unchanged. +Conflicting values are resolved in the following way: + + - "unused", "undecided" -> "undecided" + - "unused", "used" -> "used" + - "undecided, "used" -> "used" + +For for-loops, the condition, body and post-part are visited twice, taking +the joining control-flow at the condition into account. +In other words, we create three control flow paths: Zero runs of the loop, +one run and two runs and then combine them at the end. + +Simulating a third run or even more is unnecessary, which can be seen as follows: + +A state of an assignment at the beginning of the iteration will deterministically +result in a state of that assignment at the end of the iteration. Let this +state mapping function be called ``f``. The combination of the three different +states ``unused``, ``undecided`` and ``used`` as explained above is the ``max`` +operation where ``unused = 0``, ``undecided = 1`` and ``used = 2``. + +The proper way would be to compute + +:: + + max(s, f(s), f(f(s)), f(f(f(s))), ...) + +as state after the loop. Since ``f`` just has a range of three different values, +iterating it has to reach a cycle after at most three iterations, +and thus ``f(f(f(s)))`` has to equal one of ``s``, ``f(s)``, or ``f(f(s))`` +and thus + +:: + + max(s, f(s), f(f(s))) = max(s, f(s), f(f(s)), f(f(f(s))), ...). + +In summary, running the loop at most twice is enough because there are only three +different states. + +For switch statements that have a "default"-case, there is no control-flow +part that skips the switch. + +When a variable goes out of scope, all statements still in the "undecided" +state are changed to "unused", unless the variable is the return +parameter of a function - there, the state changes to "used". + +In the second traversal, all assignments that are in the "unused" state are removed. + +This step is usually run right after the SSA transform to complete +the generation of the pseudo-SSA. + +Tools +----- + +Movability +^^^^^^^^^^ + +Movability is a property of an expression. It roughly means that the expression +is side-effect free and its evaluation only depends on the values of variables +and the call-constant state of the environment. Most expressions are movable. +The following parts make an expression non-movable: + + - function calls (might be relaxed in the future if all statements in the function are movable) + - opcodes that (can) have side-effects (like ``call`` or ``selfdestruct``) + - opcodes that read or write memory, storage or external state information + - opcodes that depend on the current PC, memory size or returndata size + +DataflowAnalyzer +^^^^^^^^^^^^^^^^ + +The Dataflow Analyzer is not an optimizer step itself but is used as a tool +by other components. While traversing the AST, it tracks the current value of +each variable, as long as that value is a movable expression. +It records the variables that are part of the expression +that is currently assigned to each other variable. Upon each assignment to +a variable ``a``, the current stored value of ``a`` is updated and +all stored values of all variables ``b`` are cleared whenever ``a`` is part +of the currently stored expression for ``b``. + +At control-flow joins, knowledge about variables is cleared if they have or would be assigned +in any of the control-flow paths. For instance, upon entering a +for loop, all variables are cleared that will be assigned during the +body or the post block. + +Expression-Scale Simplifications +-------------------------------- + +These simplification passes change expressions and replace them by equivalent +and hopefully simpler expressions. + +.. _common-subexpression-eliminator: + +CommonSubexpressionEliminator +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This step uses the Dataflow Analyzer and replaces subexpressions that +syntactically match the current value of a variable by a reference to +that variable. This is an equivalence transform because such subexpressions have +to be movable. + +All subexpressions that are identifiers themselves are replaced by their +current value if the value is an identifier. + +The combination of the two rules above allow to compute a local value +numbering, which means that if two variables have the same +value, one of them will always be unused. The Unused Pruner or the +Redundant Assign Eliminator will then be able to fully eliminate such +variables. + +This step is especially efficient if the expression splitter is run +before. If the code is in pseudo-SSA form, +the values of variables are available for a longer time and thus we +have a higher chance of expressions to be replaceable. + +The expression simplifier will be able to perform better replacements +if the common subexpression eliminator was run right before it. + +.. _expression-simplifier: + +Expression Simplifier +^^^^^^^^^^^^^^^^^^^^^ + +The Expression Simplifier uses the Dataflow Analyzer and makes use +of a list of equivalence transforms on expressions like ``X + 0 -> X`` +to simplify the code. + +It tries to match patterns like ``X + 0`` on each subexpression. +During the matching procedure, it resolves variables to their currently +assigned expressions to be able to match more deeply nested patterns +even when the code is in pseudo-SSA form. + +Some of the patterns like ``X - X -> 0`` can only be applied as long +as the expression ``X`` is movable, because otherwise it would remove its potential side-effects. +Since variable references are always movable, even if their current +value might not be, the Expression Simplifier is again more powerful +in split or pseudo-SSA form. + +.. _literal-rematerialiser: + +LiteralRematerialiser +^^^^^^^^^^^^^^^^^^^^^ + +To be documented. + +.. _load-resolver: + +LoadResolver +^^^^^^^^^^^^ + +Optimisation stage that replaces expressions of type ``sload(x)`` and ``mload(x)`` by the value +currently stored in storage resp. memory, if known. + +Works best if the code is in SSA form. + +Prerequisite: Disambiguator, ForLoopInitRewriter. + +.. _reasoning-based-simplifier: + +ReasoningBasedSimplifier +^^^^^^^^^^^^^^^^^^^^^^^^ + +This optimizer uses SMT solvers to check whether ``if`` conditions are constant. + + - If ``constraints AND condition`` is UNSAT, the condition is never true and the whole body can be removed. + - If ``constraints AND NOT condition`` is UNSAT, the condition is always true and can be replaced by ``1``. + +The simplifications above can only be applied if the condition is movable. + +It is only effective on the EVM dialect, but safe to use on other dialects. + +Prerequisite: Disambiguator, SSATransform. + +Statement-Scale Simplifications +------------------------------- + +.. _circular-reference-pruner: + +CircularReferencesPruner +^^^^^^^^^^^^^^^^^^^^^^^^ + +This stage removes functions that call each other but are +neither externally referenced nor referenced from the outermost context. + +.. _conditional-simplifier: + +ConditionalSimplifier +^^^^^^^^^^^^^^^^^^^^^ + +The Conditional Simplifier inserts assignments to condition variables if the value can be determined +from the control-flow. + +Destroys SSA form. + +Currently, this tool is very limited, mostly because we do not yet have support +for boolean types. Since conditions only check for expressions being nonzero, +we cannot assign a specific value. + +Current features: + + - switch cases: insert " := " + - after if statement with terminating control-flow, insert " := 0" + +Future features: + + - allow replacements by "1" + - take termination of user-defined functions into account + +Works best with SSA form and if dead code removal has run before. + +Prerequisite: Disambiguator. + +.. _conditional-unsimplifier: + +ConditionalUnsimplifier +^^^^^^^^^^^^^^^^^^^^^^^ + +Reverse of Conditional Simplifier. + +.. _control-flow-simplifier: + +ControlFlowSimplifier +^^^^^^^^^^^^^^^^^^^^^ + +Simplifies several control-flow structures: + + - replace if with empty body with pop(condition) + - remove empty default switch case + - remove empty switch case if no default case exists + - replace switch with no cases with pop(expression) + - turn switch with single case into if + - replace switch with only default case with pop(expression) and body + - replace switch with const expr with matching case body + - replace ``for`` with terminating control flow and without other break/continue by ``if`` + - remove ``leave`` at the end of a function. + +None of these operations depend on the data flow. The StructuralSimplifier +performs similar tasks that do depend on data flow. + +The ControlFlowSimplifier does record the presence or absence of ``break`` +and ``continue`` statements during its traversal. + +Prerequisite: Disambiguator, FunctionHoister, ForLoopInitRewriter. +Important: Introduces EVM opcodes and thus can only be used on EVM code for now. + +.. _dead-code-eliminator: + +DeadCodeEliminator +^^^^^^^^^^^^^^^^^^ + +This optimization stage removes unreachable code. + +Unreachable code is any code within a block which is preceded by a +leave, return, invalid, break, continue, selfdestruct or revert. + +Function definitions are retained as they might be called by earlier +code and thus are considered reachable. + +Because variables declared in a for loop's init block have their scope extended to the loop body, +we require ForLoopInitRewriter to run before this step. + +Prerequisite: ForLoopInitRewriter, Function Hoister, Function Grouper + +.. _unused-pruner: + +UnusedPruner +^^^^^^^^^^^^ + +This step removes the definitions of all functions that are never referenced. + +It also removes the declaration of variables that are never referenced. +If the declaration assigns a value that is not movable, the expression is retained, +but its value is discarded. + +All movable expression statements (expressions that are not assigned) are removed. + +.. _structural-simplifier: + +StructuralSimplifier +^^^^^^^^^^^^^^^^^^^^ + +This is a general step that performs various kinds of simplifications on +a structural level: + + - replace if statement with empty body by ``pop(condition)`` + - replace if statement with true condition by its body + - remove if statement with false condition + - turn switch with single case into if + - replace switch with only default case by ``pop(expression)`` and body + - replace switch with literal expression by matching case body + - replace for loop with false condition by its initialization part + +This component uses the Dataflow Analyzer. + +.. _block-flattener: + +BlockFlattener +^^^^^^^^^^^^^^ + +This stage eliminates nested blocks by inserting the statement in the +inner block at the appropriate place in the outer block: + +.. code-block:: yul + + { + let x := 2 + { + let y := 3 + mstore(x, y) + } + } + +is transformed to + +.. code-block:: yul + + { + let x := 2 + let y := 3 + mstore(x, y) + } + +As long as the code is disambiguated, this does not cause a problem because +the scopes of variables can only grow. + +.. _loop-invariant-code-motion: + +LoopInvariantCodeMotion +^^^^^^^^^^^^^^^^^^^^^^^ +This optimization moves movable SSA variable declarations outside the loop. + +Only statements at the top level in a loop's body or post block are considered, i.e variable +declarations inside conditional branches will not be moved out of the loop. + +Requirements: + + - The Disambiguator, ForLoopInitRewriter and FunctionHoister must be run upfront. + - Expression splitter and SSA transform should be run upfront to obtain better result. + + +Function-Level Optimizations +---------------------------- + +.. _function-specializer: + +FunctionSpecializer +^^^^^^^^^^^^^^^^^^^ + +This step specializes the function with its literal arguments. + +If a function, say, ``function f(a, b) { sstore (a, b) }``, is called with literal arguments, for +example, ``f(x, 5)``, where ``x`` is an identifier, it could be specialized by creating a new +function ``f_1`` that takes only one argument, i.e., + +.. code-block:: yul + + function f_1(a_1) { + let b_1 := 5 + sstore(a_1, b_1) + } + +Other optimization steps will be able to make more simplifications to the function. The +optimization step is mainly useful for functions that would not be inlined. + +Prerequisites: Disambiguator, FunctionHoister + +LiteralRematerialiser is recommended as a prerequisite, even though it's not required for +correctness. + +.. _unused-function-parameter-pruner: + +UnusedFunctionParameterPruner +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This step removes unused parameters in a function. + +If a parameter is unused, like ``c`` and ``y`` in, ``function f(a,b,c) -> x, y { x := div(a,b) }``, we +remove the parameter and create a new "linking" function as follows: + +.. code-block:: yul + + function f(a,b) -> x { x := div(a,b) } + function f2(a,b,c) -> x, y { x := f(a,b) } + +and replace all references to ``f`` by ``f2``. +The inliner should be run afterwards to make sure that all references to ``f2`` are replaced by +``f``. + +Prerequisites: Disambiguator, FunctionHoister, LiteralRematerialiser. + +The step LiteralRematerialiser is not required for correctness. It helps deal with cases such as: +``function f(x) -> y { revert(y, y} }`` where the literal ``y`` will be replaced by its value ``0``, +allowing us to rewrite the function. + +.. _equivalent-function-combiner: + +EquivalentFunctionCombiner +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If two functions are syntactically equivalent, while allowing variable +renaming but not any re-ordering, then any reference to one of the +functions is replaced by the other. + +The actual removal of the function is performed by the Unused Pruner. + + +Function Inlining +----------------- + +.. _functional-inliner: + +FunctionalInliner +^^^^^^^^^^^^^^^^^ + +This component of the optimizer performs restricted function inlining by inlining functions that can be +inlined inside functional expressions, i.e. functions that: + + - return a single value. + - have a body like ``r := ``. + - neither reference themselves nor ``r`` in the right hand side. + +Furthermore, for all parameters, all of the following need to be true: + + - The argument is movable. + - The parameter is either referenced less than twice in the function body, or the argument is rather cheap + ("cost" of at most 1, like a constant up to 0xff). + +Example: The function to be inlined has the form of ``function f(...) -> r { r := E }`` where +``E`` is an expression that does not reference ``r`` and all arguments in the function call are movable expressions. + +The result of this inlining is always a single expression. + +This component can only be used on sources with unique names. + +.. _full-function-inliner: + +FullFunctionInliner +^^^^^^^^^^^^^^^^^^^ + +The Full Function Inliner replaces certain calls of certain functions +by the function's body. This is not very helpful in most cases, because +it just increases the code size but does not have a benefit. Furthermore, +code is usually very expensive and we would often rather have shorter +code than more efficient code. In same cases, though, inlining a function +can have positive effects on subsequent optimizer steps. This is the case +if one of the function arguments is a constant, for example. + +During inlining, a heuristic is used to tell if the function call +should be inlined or not. +The current heuristic does not inline into "large" functions unless +the called function is tiny. Functions that are only used once +are inlined, as well as medium-sized functions, while function +calls with constant arguments allow slightly larger functions. + + +In the future, we may include a backtracking component +that, instead of inlining a function right away, only specializes it, +which means that a copy of the function is generated where +a certain parameter is always replaced by a constant. After that, +we can run the optimizer on this specialized function. If it +results in heavy gains, the specialized function is kept, +otherwise the original function is used instead. + +Cleanup +------- + +The cleanup is performed at the end of the optimizer run. It tries +to combine split expressions into deeply nested ones again and also +improves the "compilability" for stack machines by eliminating +variables as much as possible. + +.. _expression-joiner: + +ExpressionJoiner +^^^^^^^^^^^^^^^^ + +This is the opposite operation of the expression splitter. It turns a sequence of +variable declarations that have exactly one reference into a complex expression. +This stage fully preserves the order of function calls and opcode executions. +It does not make use of any information concerning the commutativity of the opcodes; +if moving the value of a variable to its place of use would change the order +of any function call or opcode execution, the transformation is not performed. + +Note that the component will not move the assigned value of a variable assignment +or a variable that is referenced more than once. + +The snippet ``let x := add(0, 2) let y := mul(x, mload(2))`` is not transformed, +because it would cause the order of the call to the opcodes ``add`` and +``mload`` to be swapped - even though this would not make a difference +because ``add`` is movable. + +When reordering opcodes like that, variable references and literals are ignored. +Because of that, the snippet ``let x := add(0, 2) let y := mul(x, 3)`` is +transformed to ``let y := mul(add(0, 2), 3)``, even though the ``add`` opcode +would be executed after the evaluation of the literal ``3``. + +.. _SSA-reverser: + +SSAReverser +^^^^^^^^^^^ + +This is a tiny step that helps in reversing the effects of the SSA transform +if it is combined with the Common Subexpression Eliminator and the +Unused Pruner. + +The SSA form we generate is detrimental to code generation on the EVM and +WebAssembly alike because it generates many local variables. It would +be better to just re-use existing variables with assignments instead of +fresh variable declarations. + +The SSA transform rewrites + +.. code-block:: yul + + a := E + mstore(a, 1) + +to + +.. code-block:: yul + + let a_1 := E + a := a_1 + mstore(a_1, 1) + +The problem is that instead of ``a``, the variable ``a_1`` is used +whenever ``a`` was referenced. The SSA transform changes statements +of this form by just swapping out the declaration and the assignment. The above +snippet is turned into + +.. code-block:: yul + + a := E + let a_1 := a + mstore(a_1, 1) + +This is a very simple equivalence transform, but when we now run the +Common Subexpression Eliminator, it will replace all occurrences of ``a_1`` +by ``a`` (until ``a`` is re-assigned). The Unused Pruner will then +eliminate the variable ``a_1`` altogether and thus fully reverse the +SSA transform. + +.. _stack-compressor: + +StackCompressor +^^^^^^^^^^^^^^^ + +One problem that makes code generation for the Ethereum Virtual Machine +hard is the fact that there is a hard limit of 16 slots for reaching +down the expression stack. This more or less translates to a limit +of 16 local variables. The stack compressor takes Yul code and +compiles it to EVM bytecode. Whenever the stack difference is too +large, it records the function this happened in. + +For each function that caused such a problem, the Rematerialiser +is called with a special request to aggressively eliminate specific +variables sorted by the cost of their values. + +On failure, this procedure is repeated multiple times. + +.. _rematerialiser: + +Rematerialiser +^^^^^^^^^^^^^^ + +The rematerialisation stage tries to replace variable references by the expression that +was last assigned to the variable. This is of course only beneficial if this expression +is comparatively cheap to evaluate. Furthermore, it is only semantically equivalent if +the value of the expression did not change between the point of assignment and the +point of use. The main benefit of this stage is that it can save stack slots if it +leads to a variable being eliminated completely (see below), but it can also +save a DUP opcode on the EVM if the expression is very cheap. + +The Rematerialiser uses the Dataflow Analyzer to track the current values of variables, +which are always movable. +If the value is very cheap or the variable was explicitly requested to be eliminated, +the variable reference is replaced by its current value. + +.. _for-loop-condition-out-of-body: + +ForLoopConditionOutOfBody +^^^^^^^^^^^^^^^^^^^^^^^^^ + +Reverses the transformation of ForLoopConditionIntoBody. + +For any movable ``c``, it turns + +:: + + for { ... } 1 { ... } { + if iszero(c) { break } + ... + } + +into + +:: + + for { ... } c { ... } { + ... + } + +and it turns + +:: + + for { ... } 1 { ... } { + if c { break } + ... + } + +into + +:: + + for { ... } iszero(c) { ... } { + ... + } + +The LiteralRematerialiser should be run before this step. + + +WebAssembly specific +-------------------- + +MainFunction +^^^^^^^^^^^^ + +Changes the topmost block to be a function with a specific name ("main") which has no +inputs nor outputs. + +Depends on the Function Grouper. diff --git a/docs/introduction-to-smart-contracts.rst b/docs/introduction-to-smart-contracts.rst index ddcc5a83d..6b427226c 100644 --- a/docs/introduction-to-smart-contracts.rst +++ b/docs/introduction-to-smart-contracts.rst @@ -15,7 +15,7 @@ everything right now, we will go into more detail later. Storage Example =============== -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0; @@ -51,8 +51,10 @@ code that manages the database. In this example, the contract defines the functions ``set`` and ``get`` that can be used to modify or retrieve the value of the variable. -To access a state variable, you do not need the prefix ``this.`` as is common in -other languages. +To access a member (like a state variable) of the current contract, you do not typically add the ``this.`` prefix, +you just access it directly via its name. +Unlike in some other languages, omitting it is not just a matter of style, +it results in a completely different way to access the member, but more on this later. This contract does not do much yet apart from (due to the infrastructure built by Ethereum) allowing anyone to store a single number that is accessible by @@ -80,7 +82,7 @@ cryptocurrency. The contract allows only its creator to create new coins (differ Anyone can send coins to each other without a need for registering with a username and password, all you need is an Ethereum keypair. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.4; @@ -224,8 +226,8 @@ a failure can more easily be debugged or reacted upon. The ``send`` function can be used by anyone (who already has some of these coins) to send coins to anyone else. If the sender does not have -enough coins to send, the ``require`` call fails and provides the -sender with an appropriate error message string. +enough coins to send, the ``if`` condition evaluates to true. As a result, the ``revert`` will cause the operation to fail +while providing the sender with error details using the ``InsufficientBalance`` error. .. note:: If you use diff --git a/docs/ir/ir-breaking-changes.rst b/docs/ir/ir-breaking-changes.rst index 2d5345080..7d6b6d54f 100644 --- a/docs/ir/ir-breaking-changes.rst +++ b/docs/ir/ir-breaking-changes.rst @@ -14,7 +14,8 @@ hiding new and different behavior in existing code. * When storage structs are deleted, every storage slot that contains a member of the struct is set to zero entirely. Formally, padding space was left untouched. Consequently, if the padding space within a struct is used to store data (e.g. in the context of a contract upgrade), you have to be aware that ``delete`` will now also clear the added member (while it wouldn't have been cleared in the past). -:: +.. code-block:: solidity + // SPDX-License-Identifier: GPL-3.0 pragma solidity >0.7.0; @@ -42,7 +43,8 @@ We have the same behavior for implicit delete, for example when array of structs The new code generator implements modifiers using actual functions and passes function parameters on. This means that multiple executions of a function will get the same values for the parameters. -:: +.. code-block:: solidity + // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0; contract C { @@ -71,7 +73,9 @@ New order: 2. Constructor, if present. This causes differences in some contracts, for example: -:: + +.. code-block:: solidity + // SPDX-License-Identifier: GPL-3.0 pragma solidity >0.7.0; @@ -93,7 +97,9 @@ With the new rules, ``y`` will be set to 42. We first initialize ``x`` to 0, the * Copying ``bytes`` arrays from memory to storage is implemented in a different way. The old code generator always copies full words, while the new one cuts the byte array after its end. The old behaviour can lead to dirty data being copied after the end of the array (but still in the same storage slot). This causes differences in some contracts, for example: -:: + +.. code-block:: solidity + // SPDX-License-Identifier: GPL-3.0 pragma solidity >0.8.0; @@ -123,7 +129,8 @@ Now it is returning ``0x64656164626565660000000000000000000000000000000000000000 For example: -:: +.. code-block:: solidity + // SPDX-License-Identifier: GPL-3.0 pragma solidity >0.8.0; contract C { @@ -138,10 +145,11 @@ The function ``preincr_u8(1)`` returns the following values: .. index:: ! evaluation order; function arguments -On the other hand, function argument expressions are evaluated in the same order by both code generators. +On the other hand, function argument expressions are evaluated in the same order by both code generators with the exception of the global functions ``addmod`` and ``mulmod``. For example: -:: +.. code-block:: solidity + // SPDX-License-Identifier: GPL-3.0 pragma solidity >0.8.0; contract C { @@ -157,6 +165,27 @@ The function ``g(1, 2)`` returns the following values: - Old code generator: ``10`` (``add(2 + 3, 2 + 3)``) but the return value is unspecified in general - New code generator: ``10`` but the return value is not guaranteed +The arguments to the global functions ``addmod`` and ``mulmod`` are evaluated right-to-left by the old code generator +and left-to-right by the new code generator. +For example: + +:: + // SPDX-License-Identifier: GPL-3.0 + pragma solidity >0.8.0; + contract C { + function f() public pure returns (uint256 aMod, uint256 mMod) { + uint256 x = 3; + // Old code gen: add/mulmod(5, 4, 3) + // New code gen: add/mulmod(4, 5, 5) + aMod = addmod(++x, ++x, x); + mMod = mulmod(++x, ++x, x); + } + } + +The function ``f()`` returns the following values: +- Old code generator: ``aMod = 0`` and ``mMod = 2`` +- New code generator: ``aMod = 4`` and ``mMod = 0`` + Internals ========= @@ -188,7 +217,9 @@ The old code generator only performs cleanup before an operation whose result co The new code generator performs cleanup after any operation that can result in dirty bits. For example: -:: + +.. code-block:: solidity + // SPDX-License-Identifier: GPL-3.0 pragma solidity >0.8.0; contract C { diff --git a/docs/layout-of-source-files.rst b/docs/layout-of-source-files.rst index fb9844c65..22624c410 100644 --- a/docs/layout-of-source-files.rst +++ b/docs/layout-of-source-files.rst @@ -163,7 +163,7 @@ The component does not yet support all features of the Solidity language and likely outputs many warnings. In case it reports unsupported features, the analysis may not be fully sound. -.. index:: source file, ! import, module +.. index:: source file, ! import, module, source unit .. _import: @@ -184,6 +184,7 @@ At a global level, you can use import statements of the following form: import "filename"; +The ``filename`` part is called an *import path*. This statement imports all global symbols from "filename" (and symbols imported there) into the current global scope (different than in ES6 but backwards-compatible for Solidity). This form is not recommended for use, because it unpredictably pollutes the namespace. @@ -216,101 +217,34 @@ the code below creates new global symbols ``alias`` and ``symbol2`` which refere import {symbol1 as alias, symbol2} from "filename"; -Paths ------ +.. index:: virtual filesystem, source unit name, import; path, filesystem path, import callback, Remix IDE -In the above, ``filename`` is always treated as a path with ``/`` as directory separator, -and ``.`` as the current and ``..`` as the parent directory. When ``.`` or ``..`` is followed by a character except ``/``, -it is not considered as the current or the parent directory. -All path names are treated as absolute paths unless they start with the current ``.`` or the parent directory ``..``. +Import Paths +------------ -To import a file ``filename`` from the same directory as the current file, use ``import "./filename" as symbolName;``. -If you use ``import "filename" as symbolName;`` instead, a different file could be referenced -(in a global "include directory"). +In order to be able to support reproducible builds on all platforms, the Solidity compiler has to +abstract away the details of the filesystem where source files are stored. +For this reason import paths do not refer directly to files in the host filesystem. +Instead the compiler maintains an internal database (*virtual filesystem* or *VFS* for short) where +each source unit is assigned a unique *source unit name* which is an opaque and unstructured identifier. +The import path specified in an import statement is translated into a source unit name and used to +find the corresponding source unit in this database. -It depends on the compiler (see :ref:`import-compiler`) how to actually resolve the paths. -In general, the directory hierarchy does not need to strictly map onto your local -filesystem, and the path can also map to resources such as ipfs, http or git. +Using the :ref:`Standard JSON ` API it is possible to directly provide the names and +content of all the source files as a part of the compiler input. +In this case source unit names are truly arbitrary. +If, however, you want the compiler to automatically find and load source code into the VFS, your +source unit names need to be structured in a way that makes it possible for an :ref:`import callback +` to locate them. +When using the command-line compiler the default import callback supports only loading source code +from the host filesystem, which means that your source unit names must be paths. +Some environments provide custom callbacks that are more versatile. +For example the `Remix IDE `_ provides one that +lets you `import files from HTTP, IPFS and Swarm URLs or refer directly to packages in NPM registry +`_. -.. note:: - Always use relative imports like ``import "./filename.sol";`` and avoid - using ``..`` in path specifiers. In the latter case, it is probably better to use - global paths and set up remappings as explained below. - -.. _import-compiler: - -Use in Actual Compilers ------------------------ - -When invoking the compiler, you can specify how to discover the first element -of a path, and also path prefix remappings. For -example you can setup a remapping so that everything imported from the virtual -directory ``github.com/ethereum/dapp-bin/library`` would actually be read from -your local directory ``/usr/local/dapp-bin/library``. -If multiple remappings apply, the one with the longest key is tried first. -An empty prefix is not allowed. The remappings can depend on a context, -which allows you to configure packages to import e.g., different versions of a -library of the same name. - -**solc**: - -For solc (the commandline compiler), you provide these path remappings as -``context:prefix=target`` arguments, where both the ``context:`` and the -``=target`` parts are optional (``target`` defaults to ``prefix`` in this -case). All remapping values that are regular files are compiled (including -their dependencies). - -This mechanism is backwards-compatible (as long -as no filename contains ``=`` or ``:``) and thus not a breaking change. All -files in or below the ``context`` directory that import a file that starts with -``prefix`` are redirected by replacing ``prefix`` by ``target``. - -For example, if you clone ``github.com/ethereum/dapp-bin/`` locally to -``/usr/local/dapp-bin``, you can use the following in your source file: - -:: - - import "github.com/ethereum/dapp-bin/library/iterable_mapping.sol" as it_mapping; - -Then run the compiler: - -.. code-block:: bash - - solc github.com/ethereum/dapp-bin/=/usr/local/dapp-bin/ source.sol - -As a more complex example, suppose you rely on a module that uses an old -version of dapp-bin that you checked out to ``/usr/local/dapp-bin_old``, then you can run: - -.. code-block:: bash - - solc module1:github.com/ethereum/dapp-bin/=/usr/local/dapp-bin/ \ - module2:github.com/ethereum/dapp-bin/=/usr/local/dapp-bin_old/ \ - source.sol - -This means that all imports in ``module2`` point to the old version but imports -in ``module1`` point to the new version. - -.. note:: - - ``solc`` only allows you to include files from certain directories. They have - to be in the directory (or subdirectory) of one of the explicitly specified - source files or in the directory (or subdirectory) of a remapping target. If - you want to allow direct absolute includes, add the remapping ``/=/``. - -If there are multiple remappings that lead to a valid file, the remapping -with the longest common prefix is chosen. - -**Remix**: - -`Remix `_ provides an automatic remapping for -GitHub and automatically retrieves the file over the network. You can import -the iterable mapping as above, e.g. - -:: - - import "github.com/ethereum/dapp-bin/library/iterable_mapping.sol" as it_mapping; - -Remix may add other source code providers in the future. +For a complete description of the virtual filesystem and the path resolution logic used by the +compiler see :ref:`Path Resolution `. .. index:: ! comment, natspec diff --git a/docs/natspec-format.rst b/docs/natspec-format.rst index de05f6858..7007fe4a1 100644 --- a/docs/natspec-format.rst +++ b/docs/natspec-format.rst @@ -72,7 +72,7 @@ The following example shows a contract and a function using all available tags. /// @notice Calculate tree age in years, rounded up, for live trees /// @dev The Alexandr N. Tetearing algorithm could increase precision /// @param rings The number of rings from dendrochronological sample - /// @return age in years, rounded up for partial years + /// @return Age in years, rounded up for partial years function age(uint256 rings) external virtual pure returns (uint256) { return rings + 1; } @@ -115,10 +115,10 @@ in the same way as if it were tagged with ``@notice``. =============== ====================================================================================== ============================= Tag Context =============== ====================================================================================== ============================= -``@title`` A title that should describe the contract/interface contract, interface -``@author`` The name of the author contract, interface -``@notice`` Explain to an end user what this does contract, interface, function, public state variable, event -``@dev`` Explain to a developer any extra details contract, interface, function, state variable, event +``@title`` A title that should describe the contract/interface contract, library, interface +``@author`` The name of the author contract, library, interface +``@notice`` Explain to an end user what this does contract, library, interface, function, public state variable, event +``@dev`` Explain to a developer any extra details contract, library, interface, function, state variable, event ``@param`` Documents a parameter just like in Doxygen (must be followed by parameter name) function, event ``@return`` Documents the return variables of a contract's function function, public state variable ``@inheritdoc`` Copies all missing tags from the base function (must be followed by the contract name) function, public state variable diff --git a/docs/path-resolution.rst b/docs/path-resolution.rst new file mode 100644 index 000000000..5848e3f9a --- /dev/null +++ b/docs/path-resolution.rst @@ -0,0 +1,486 @@ +.. _path-resolution: + +********************** +Import Path Resolution +********************** + +In order to be able to support reproducible builds on all platforms, the Solidity compiler has to +abstract away the details of the filesystem where source files are stored. +Paths used in imports must work the same way everywhere while the command-line interface must be +able to work with platform-specific paths to provide good user experience. +This section aims to explain in detail how Solidity reconciles these requirements. + +.. index:: ! virtual filesystem, ! VFS, ! source unit name +.. _virtual-filesystem: + +Virtual Filesystem +================== + +The compiler maintains an internal database (*virtual filesystem* or *VFS* for short) where each +source unit is assigned a unique *source unit name* which is an opaque and unstructured identifier. +When you use the :ref:`import statement `, you specify an *import path* that references a +source unit name. + +.. index:: ! import callback, ! Host Filesystem Loader +.. _import-callback: + +Import Callback +--------------- + +The VFS is initially populated only with files the compiler has received as input. +Additional files can be loaded during compilation using an *import callback*, which is different +depending on the type of compiler you use (see below). +If the compiler does not find any source unit name matching the import path in the VFS, it invokes +the callback, which is responsible for obtaining the source code to be placed under that name. +An import callback is free to interpret source unit names in an arbitrary way, not just as paths. +If there is no callback available when one is needed or if it fails to locate the source code, +compilation fails. + +The command-line compiler provides the *Host Filesystem Loader* - a rudimentary callback +that interprets a source unit name as a path in the local filesystem. +The `JavaScript interface `_ does not provide any by default, +but one can be provided by the user. +This mechanism can be used to obtain source code from locations other then the local filesystem +(which may not even be accessible, e.g. when the compiler is running in a browser). +For example the `Remix IDE `_ provides a versatile callback that +lets you `import files from HTTP, IPFS and Swarm URLs or refer directly to packages in NPM registry +`_. + +.. note:: + + Host Filesystem Loader's file lookup is platform-dependent. + For example backslashes in a source unit name can be interpreted as directory separators or not + and the lookup can be case-sensitive or not, depending on the underlying platform. + + For portability it is recommended to avoid using import paths that will work correctly only + with a specific import callback or only on one platform. + For example you should always use forward slashes since they work as path separators also on + platforms that support backslashes. + +Initial Content of the Virtual Filesystem +----------------------------------------- + +The initial content of the VFS depends on how you invoke the compiler: + +#. **solc / command-line interface** + + When you compile a file using the command-line interface of the compiler, you provide one or + more paths to files containing Solidity code: + + .. code-block:: bash + + solc contract.sol /usr/local/dapp-bin/token.sol + + The source unit name of a file loaded this way is simply the specified path after shell expansion + and with platform-specific separators converted to forward slashes. + + .. index:: standard JSON + +#. **Standard JSON** + + When using the :ref:`Standard JSON ` API (via either the `JavaScript interface + `_ or the ``--standard-json`` command-line option) + you provide input in JSON format, containing, among other things, the content of all your source + files: + + .. code-block:: json + + { + "language": "Solidity", + "sources": { + "contract.sol": { + "content": "import \"./util.sol\";\ncontract C {}" + }, + "util.sol": { + "content": "library Util {}" + }, + "/usr/local/dapp-bin/token.sol": { + "content": "contract Token {}" + } + }, + "settings": {"outputSelection": {"*": { "*": ["metadata", "evm.bytecode"]}}} + } + + The ``sources`` dictionary becomes the initial content of the virtual filesystem and its keys + are used as source unit names. + + .. _initial-vfs-content-standard-json-with-import-callback: + +#. **Standard JSON (via import callback)** + + With Standard JSON it is also possible to tell the compiler to use the import callback to obtain + the source code: + + .. code-block:: json + + { + "language": "Solidity", + "sources": { + "/usr/local/dapp-bin/token.sol": { + "urls": [ + "/projects/mytoken.sol", + "https://example.com/projects/mytoken.sol" + ] + } + }, + "settings": {"outputSelection": {"*": { "*": ["metadata", "evm.bytecode"]}}} + } + + If an import callback is available, the compiler will give it the strings specified in + ``urls`` one by one, until one is loaded successfully or the end of the list is reached. + + The source unit names are determined the same way as when using ``content`` - they are keys of + the ``sources`` dictionary and the content of ``urls`` does not affect them in any way. + + .. index:: standard input, stdin, + +#. **Standard input** + + On the command line it is also possible to provide the source by sending it to compiler's + standard input: + + .. code-block:: bash + + echo 'import "./util.sol"; contract C {}' | solc - + + ``-`` used as one of the arguments instructs the compiler to place the content of the standard + input in the virtual filesystem under a special source unit name: ````. + +Once the VFS is initialized, additional files can still be added to it only through the import +callback. + +.. index:: ! import; path + +Imports +======= + +The import statement specifies an *import path*. +Based on how the import path is specified, we can divide imports into two categories: + +- :ref:`Direct imports `, where you specify the full source unit name directly. +- :ref:`Relative imports `, where you specify a path starting with ``./`` or ``../`` + to be combined with the source unit name of the importing file. + + +.. code-block:: solidity + :caption: contracts/contract.sol + + import "./math/math.sol"; + import "contracts/tokens/token.sol"; + +In the above ``./math/math.sol`` and ``contracts/tokens/token.sol`` are import paths while the +source unit names they translate to are ``contracts/math/math.sol`` and ``contracts/tokens/token.sol`` +respectively. + +.. index:: ! direct import, import; direct +.. _direct-imports: + +Direct Imports +-------------- + +An import that does not start with ``./`` or ``../`` is a *direct import*. + +:: + + import "/project/lib/util.sol"; // source unit name: /project/lib/util.sol + import "lib/util.sol"; // source unit name: lib/util.sol + import "@openzeppelin/address.sol"; // source unit name: @openzeppelin/address.sol + import "https://example.com/token.sol"; // source unit name: https://example.com/token.sol + +After applying any :ref:`import remappings ` the import path simply becomes the +source unit name. + +.. note:: + + A source unit name is just an identifier and even if its value happens to look like a path, it + is not subject to the normalization rules you would typically expect in a shell. + Any ``/./`` or ``/../`` seguments or sequences of multiple slashes remain a part of it. + When the source is provided via Standard JSON interface it is entirely possible to associate + different content with source unit names that would refer to the same file on disk. + +When the source is not available in the virtual filesystem, the compiler passes the source unit name +to the import callback. +The Host Filesystem Loader will attempt to use it as a path and look up the file on disk. +At this point the platform-specific normalization rules kick in and names that were considered +different in the VFS may actually result in the same file being loaded. +For example ``/project/lib/math.sol`` and ``/project/lib/../lib///math.sol`` are considered +completely different in the VFS even though they refer to the same file on disk. + +.. note:: + + Even if an import callback ends up loading source code for two different source unit names from + the same file on disk, the compiler will still see them as separate source units. + It is the source unit name that matters, not the physical location of the code. + +.. index:: ! relative import, ! import; relative +.. _relative-imports: + +Relative Imports +---------------- + +An import starting with ``./`` or ``../`` is a *relative import*. +Such imports specify a path relative to the source unit name of the importing source unit: + +.. code-block:: solidity + :caption: /project/lib/math.sol + + import "./util.sol" as util; // source unit name: /project/lib/util.sol + import "../token.sol" as token; // source unit name: /project/token.sol + +.. code-block:: solidity + :caption: lib/math.sol + + import "./util.sol" as util; // source unit name: lib/util.sol + import "../token.sol" as token; // source unit name: token.sol + +.. note:: + + Relative imports **always** start with ``./`` or ``../`` so ``import "util.sol"``, unlike + ``import "./util.sol"``, is a direct import. + While both paths would be considered relative in the host filesystem, ``util.sol`` is actually + absolute in the VFS. + +Let us define a *path segment* as any non-empty part of the path that does not contain a separator +and is bounded by two path separators. +A separator is a forward slash or the beginning/end of the string. +For example in ``./abc/..//`` there are three path segments: ``.``, ``abc`` and ``..``. + +The compiler computes a source unit name from the import path in the following way: + +1. First a prefix is computed + + - Prefix is initialized with the source unit name of the importing source unit. + - The last path segment with preceding slashes is removed from the prefix. + - Then, the leading part of the normalized import path, consisting only of ``/`` and ``.`` + characters is considered. + For every ``..`` segment found in this part the last path segment with preceding slashes is + removed from the prefix. + +2. Then the prefix is prepended to the normalized import path. + If the prefix is non-empty, a single slash is inserted between it and the import path. + +The removal of the last path segment with preceding slashes is understood to +work as follows: + +1. Everything past the last slash is removed (i.e. ``a/b//c.sol`` becomes ``a/b//``). +2. All trailing slashes are removed (i.e. ``a/b//`` becomes ``a/b``). + +The normalization rules are the same as for UNIX paths, namely: + +- All the internal ``.`` segments are removed. +- Every internal ``..`` segment backtracks one level up in the hierarchy. +- Multiple slashes are squashed into a single one. + +Note that normalization is performed only on the import path. +The source unit name of the importing module that is used for the prefix remains unnormalized. +This ensures that the ``protocol://`` part does not turn into ``protocol:/`` if the importing file +is identified with a URL. + +If your import paths are already normalized, you can expect the above algorithm to produce very +intuitive results. +Here are some examples of what you can expect if they are not: + +.. code-block:: solidity + :caption: lib/src/../contract.sol + + import "./util/./util.sol"; // source unit name: lib/src/../util/util.sol + import "./util//util.sol"; // source unit name: lib/src/../util/util.sol + import "../util/../array/util.sol"; // source unit name: lib/src/array/util.sol + import "../.././../util.sol"; // source unit name: util.sol + import "../../.././../util.sol"; // source unit name: util.sol + +.. note:: + + The use of relative imports containing leading ``..`` segments is not recommended. + The same effect can be achieved in a more reliable way by using direct imports with + :ref:`base path ` and :ref:`import remapping `. + +.. index:: ! base path, --base-path +.. _base-path: + +Base Path +========= + +The base path specifies the directory that the Host Filesystem Loader will load files from. +It is simply prepended to a source unit name before the filesystem lookup is performed. + +By default the base path is empty, which leaves the source unit name unchanged. +When the source unit name is a relative path, this results in the file being looked up in the +directory the compiler has been invoked from. +It is also the only value that results in absolute paths in source unit names being actually +interpreted as absolute paths on disk. + +If the base path itself is relative, it is also interpreted as relative to the current working +directory of the compiler. + +.. index:: ! remapping; import, ! import; remapping, ! remapping; context, ! remapping; prefix, ! remapping; target +.. _import-remapping: + +Import Remapping +================ + +Import remapping allows you to redirect imports to a different location in the virtual filesystem. +The mechanism works by changing the translation between import paths and source unit names. +For example you can set up a remapping so that any import from the virtual directory +``github.com/ethereum/dapp-bin/library/`` would be seen as an import from ``dapp-bin/library/`` instead. + +You can limit the scope of a remapping by specifying a *context*. +This allows creating remappings that apply only to imports located in a specific library or a specific file. +Without a context a remapping is applied to every matching import in all the files in the virtual +filesystem. + +Import remappings have the form of ``context:prefix=target``: + +- ``context`` must match the beginning of the source unit name of the file containing the import. +- ``prefix`` must match the beginning of the source unit name resulting from the import. +- ``target`` is the value the prefix is replaced with. + +For example, if you clone https://github.com/ethereum/dapp-bin/ locally to ``/project/dapp-bin`` +and run the compiler with: + +.. code-block:: bash + + solc github.com/ethereum/dapp-bin/=dapp-bin/ --base-path /project source.sol + +you can use the following in your source file: + +.. code-block:: solidity + + import "github.com/ethereum/dapp-bin/library/math.sol"; // source unit name: dapp-bin/library/math.sol + +The compiler will look for the file in the VFS under ``dapp-bin/library/math.sol``. +If the file is not available there, the source unit name will be passed to the Host Filesystem +Loader, which will then look in ``/project/dapp-bin/library/iterable_mapping.sol``. + +.. warning:: + + Information about remappings is stored in contract metadata. + Since the binary produced by the compiler has a hash of the metadata embedded in it, any + modification to the remappings will result in different bytecode. + + For this reason you should be careful not to include any local information in remapping targets. + For example if your library is located in ``/home/user/packages/mymath/math.sol``, a remapping + like ``@math/=/home/user/packages/mymath/`` would result in your home directory being included in + the metadata. + To be able to reproduce the same bytecode with such a remapping on a different machine, you + would need to recreate parts of your local directory structure in the VFS and (if you rely on + Host Filesystem Loader) also in the host filesystem. + +As a more complex example, suppose you rely on a module that uses an old version of dapp-bin that +you checked out to ``/project/dapp-bin_old``, then you can run: + +.. code-block:: bash + + solc module1:github.com/ethereum/dapp-bin/=dapp-bin/ \ + module2:github.com/ethereum/dapp-bin/=dapp-bin_old/ \ + --base-path /project \ + source.sol + +This means that all imports in ``module2`` point to the old version but imports in ``module1`` +point to the new version. + +Here are the detailed rules governing the behaviour of remappings: + +#. **Remappings only affect the translation between import paths and source unit names.** + + Source unit names added to the VFS in any other way cannot be remapped. + For example the paths you specify on the command-line and the ones in ``sources.urls`` in + Standard JSON are not affected. + + .. code-block:: bash + + solc /project/=/contracts/ /project/contract.sol # source unit name: /project/contract.sol + + In the example above the compiler will load the source code from ``/project/contract.sol`` and + place it under that exact source unit name in the VFS, not under ``/contract/contract.sol``. + +#. **Context and prefix must match source unit names, not import paths.** + + - This means that you cannot remap ``./`` or ``../`` directly since they are replaced during + the translation to source unit name but you can remap the part of the name they are replaced + with: + + .. code-block:: bash + + solc ./=a/ /project/=b/ /project/contract.sol # source unit name: /project/contract.sol + + .. code-block:: solidity + :caption: /project/contract.sol + + import "./util.sol" as util; // source unit name: b/util.sol + + - You cannot remap base path or any other part of the path that is only added internally by an + import callback: + + .. code-block:: bash + + solc /project/=/contracts/ /project/contract.sol --base-path /project # source unit name: /project/contract.sol + + .. code-block:: solidity + :caption: /project/contract.sol + + import "util.sol" as util; // source unit name: util.sol + +#. **Target is inserted directly into the source unit name and does not necessarily have to be a valid path.** + + - It can be anything as long as the import callback can handle it. + In case of the Host Filesystem Loader this includes also relative paths. + When using the JavaScript interface you can even use URLs and abstract identifiers if + your callback can handle them. + + - Remapping happens after relative imports have already been resolved into source unit names. + This means that targets starting with ``./`` and ``../`` have no special meaning and are + relative to the base path rather than to the location of the source file. + + - Remapping targets are not normalized so ``@root/=./a/b//`` will remap ``@root/contract.sol`` + to ``./a/b//contract.sol`` and not ``a/b/contract.sol``. + + - If the target does not end with a slash, the compiler will not add one automatically: + + .. code-block:: bash + + solc /project/=/contracts /project/contract.sol # source unit name: /project/contract.sol + + .. code-block:: solidity + :caption: /project/contract.sol + + import "/project/util.sol" as util; // source unit name: /contractsutil.sol + +#. **Context and prefix are patterns and matches must be exact.** + + - ``a//b=c`` will not match ``a/b``. + - source unit names are not normalized so ``a/b=c`` will not match ``a//b`` either. + - Parts of file and directory names can match as well. + ``/newProject/con:/new=old`` will match ``/newProject/contract.sol`` and remap it to + ``oldProject/contract.sol``. + +#. **At most one remapping is applied to a single import.** + + - If multiple remappings match the same source unit name, the one with the longest matching + prefix is chosen. + - If prefixes are identical, the one specified last wins. + - Remappings do not work on other remappings. For example ``a=b b=c c=d`` will not result in ``a`` + being remapped to ``d``. + +#. **Prefix cannot be empty but context and target are optional.** + + If ``target`` is omitted, it defaults to the value of the ``prefix``. + +.. index:: Remix IDE, file:// + +Using URLs in imports +===================== + +Most URL prefixes such as ``https://`` or ``data://`` have no special meaning in import paths. +The only exception is ``file://`` which is stripped from source unit names by the Host Filesystem +Loader. + +When compiling locally you can use import remapping to replace the protocol and domain part with a +local path: + +.. code-block:: bash + + solc :https://github.com/ethereum/dapp-bin=/usr/local/dapp-bin contract.sol + +Note the leading ``:``, which is necessary when the remapping context is empty. +Otherwise the ``https:`` part would be interpreted by the compiler as the context. diff --git a/docs/requirements.txt b/docs/requirements.txt index 268c6e161..038c22b7c 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,6 @@ sphinx_rtd_theme>=0.3.1 pygments-lexer-solidity>=0.7.0 sphinx-a4doc>=1.2.1 + +# Sphinx 2.1.0 is the oldest version that accepts a lexer class in add_lexer() +sphinx>=2.1.0 diff --git a/docs/robots.txt.template b/docs/robots.txt.template new file mode 100644 index 000000000..3b7431d74 --- /dev/null +++ b/docs/robots.txt.template @@ -0,0 +1,15 @@ +User-Agent: * +Sitemap: http://docs.soliditylang.org/sitemap.xml +Host: docs.soliditylang.org + +Allow: /en/latest/ +Allow: /en/v0.7.6/ +Allow: /en/v{{ LATEST_VERSION }}/ +Allow: /_/downloads/en/latest/ +Allow: /_/downloads/en/0.7.6/ +Allow: /_/downloads/en/{{ LATEST_VERSION }}/ + +# Prevent documentation for the development branches and older Solidity +# versions from showing up in search results. +Disallow: /en/* +Disallow: /_/downloads/en/* diff --git a/docs/security-considerations.rst b/docs/security-considerations.rst index e807d301e..c59f4c750 100644 --- a/docs/security-considerations.rst +++ b/docs/security-considerations.rst @@ -56,7 +56,7 @@ to call back into A before this interaction is completed. To give an example, the following code contains a bug (it is just a snippet and not a complete contract): -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.0 <0.9.0; @@ -80,7 +80,7 @@ basically retrieve all the Ether in the contract. In particular, the following contract will allow an attacker to refund multiple times as it uses ``call`` which forwards all remaining gas by default: -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.2 <0.9.0; @@ -100,7 +100,7 @@ as it uses ``call`` which forwards all remaining gas by default: To avoid re-entrancy, you can use the Checks-Effects-Interactions pattern as outlined further below: -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.0 <0.9.0; @@ -198,7 +198,7 @@ tx.origin Never use tx.origin for authorization. Let's say you have a wallet contract like this: -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; @@ -218,7 +218,7 @@ Never use tx.origin for authorization. Let's say you have a wallet contract like Now someone tricks you into sending Ether to the address of this attack wallet: -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; @@ -251,7 +251,7 @@ They resemble integers when the values are small, but cannot represent arbitrari The following code causes an overflow because the result of the addition is too large to be stored in the type ``uint8``: -:: +.. code-block:: solidity uint8 x = 255; uint8 y = 1; @@ -289,7 +289,7 @@ field of a ``struct`` that is the base type of a dynamic storage array. The ``mapping`` is also ignored in assignments of structs or arrays containing a ``mapping``. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.0 <0.9.0; @@ -346,7 +346,7 @@ Recommendations Take Warnings Seriously ======================= -If the compiler warns you about something, you should better change it. +If the compiler warns you about something, you should change it. Even if you do not think that this particular warning has security implications, there might be another issue buried beneath it. Any compiler warning we issue can be silenced by slight changes to the diff --git a/docs/smtchecker.rst b/docs/smtchecker.rst index 6ce369bc6..6fc0a1c57 100644 --- a/docs/smtchecker.rst +++ b/docs/smtchecker.rst @@ -503,6 +503,18 @@ which has the following form: .. _smtchecker_engines: +Natspec Function Abstraction +============================ + +Certain functions including common math methods such as ``pow`` +and ``sqrt`` may be too complex to be analyzed in a fully automated way. +These functions can be annotated with Natspec tags that indicate to the +SMTChecker that these functions should be abstracted. This means that the +body of the function is not used, and when called, the function will: + +- Return a nondeterministic value, and either keep the state variables unchanged if the abstracted function is view/pure, or also set the state variables to nondeterministic values otherwise. This can be used via the annotation ``/// @custom:smtchecker abstract-function-nondet``. +- Act as an uninterpreted function. This means that the semantics of the function (given by the body) are ignored, and the only property this function has is that given the same input it guarantees the same output. This is currently under development and will be available via the annotation ``/// @custom:smtchecker abstract-function-uf``. + Model Checking Engines ====================== @@ -654,7 +666,7 @@ the arguments. Using abstraction means loss of precise knowledge, but in many cases it does not mean loss of proving power. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0; @@ -701,7 +713,7 @@ location is erased. If the type is nested, the knowledge removal also includes all the prefix base types. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0; diff --git a/docs/structure-of-a-contract.rst b/docs/structure-of-a-contract.rst index ae6f41e02..e776c4eca 100644 --- a/docs/structure-of-a-contract.rst +++ b/docs/structure-of-a-contract.rst @@ -24,7 +24,7 @@ State Variables State variables are variables whose values are permanently stored in contract storage. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; @@ -47,7 +47,7 @@ Functions are the executable units of code. Functions are usually defined inside a contract, but they can also be defined outside of contracts. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >0.7.0 <0.9.0; @@ -81,7 +81,7 @@ is not possible. Like functions, modifiers can be :ref:`overridden `. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.22 <0.9.0; @@ -109,7 +109,7 @@ Events Events are convenience interfaces with the EVM logging facilities. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.21 <0.9.0; @@ -137,7 +137,7 @@ In comparison to string descriptions, errors are much cheaper and allow you to encode additional data. You can use NatSpec to describe the error to the user. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.4; @@ -168,7 +168,7 @@ Struct Types Structs are custom defined types that can group several variables (see :ref:`structs` in types section). -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; @@ -190,7 +190,7 @@ Enum Types Enums can be used to create custom types with a finite set of 'constant values' (see :ref:`enums` in types section). -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; diff --git a/docs/style-guide.rst b/docs/style-guide.rst index 34cf6c5cb..edb3b25ee 100644 --- a/docs/style-guide.rst +++ b/docs/style-guide.rst @@ -53,7 +53,9 @@ Blank Lines Surround top level declarations in solidity source with two blank lines. -Yes:: +Yes: + +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; @@ -72,7 +74,9 @@ Yes:: // ... } -No:: +No: + +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; @@ -92,7 +96,9 @@ Within a contract surround function declarations with a single blank line. Blank lines may be omitted between groups of related one-liners (such as stub functions for an abstract contract) -Yes:: +Yes: + +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.0 <0.9.0; @@ -113,7 +119,9 @@ Yes:: } } -No:: +No: + +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.0 <0.9.0; @@ -150,7 +158,9 @@ Wrapped lines should conform to the following guidelines. Function Calls -Yes:: +Yes: + +.. code-block:: solidity thisFunctionCallIsReallyLong( longArgument1, @@ -158,7 +168,9 @@ Yes:: longArgument3 ); -No:: +No: + +.. code-block:: solidity thisFunctionCallIsReallyLong(longArgument1, longArgument2, @@ -188,7 +200,9 @@ No:: Assignment Statements -Yes:: +Yes: + +.. code-block:: solidity thisIsALongNestedMapping[being][set][to_some_value] = someFunction( argument1, @@ -197,7 +211,9 @@ Yes:: argument4 ); -No:: +No: + +.. code-block:: solidity thisIsALongNestedMapping[being][set][to_some_value] = someFunction(argument1, argument2, @@ -206,7 +222,9 @@ No:: Event Definitions and Event Emitters -Yes:: +Yes: + +.. code-block:: solidity event LongAndLotsOfArgs( address sender, @@ -224,7 +242,9 @@ Yes:: options ); -No:: +No: + +.. code-block:: solidity event LongAndLotsOfArgs(address sender, address recipient, @@ -248,7 +268,9 @@ Imports Import statements should always be placed at the top of the file. -Yes:: +Yes: + +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; @@ -263,7 +285,9 @@ Yes:: // ... } -No:: +No: + +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; @@ -297,7 +321,9 @@ Functions should be grouped according to their visibility and ordered: Within a grouping, place the ``view`` and ``pure`` functions last. -Yes:: +Yes: + +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; @@ -333,7 +359,9 @@ Yes:: // ... } -No:: +No: + +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; @@ -370,11 +398,15 @@ Avoid extraneous whitespace in the following situations: Immediately inside parenthesis, brackets or braces, with the exception of single line function declarations. -Yes:: +Yes: + +.. code-block:: solidity spam(ham[1], Coin({name: "ham"})); -No:: +No: + +.. code-block:: solidity spam( ham[ 1 ], Coin( { name: "ham" } ) ); @@ -384,23 +416,31 @@ Exception:: Immediately before a comma, semicolon: -Yes:: +Yes: + +.. code-block:: solidity function spam(uint i, Coin coin) public; -No:: +No: + +.. code-block:: solidity function spam(uint i , Coin coin) public ; More than one space around an assignment or other operator to align with another: -Yes:: +Yes: + +.. code-block:: solidity x = 1; y = 2; long_variable = 3; -No:: +No: + +.. code-block:: solidity x = 1; y = 2; @@ -408,7 +448,9 @@ No:: Don't include a whitespace in the receive and fallback functions: -Yes:: +Yes: + +.. code-block:: solidity receive() external payable { ... @@ -418,7 +460,9 @@ Yes:: ... } -No:: +No: + +.. code-block:: solidity receive () external payable { ... @@ -440,7 +484,9 @@ should: declaration. * The opening brace should be preceded by a single space. -Yes:: +Yes: + +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; @@ -452,7 +498,9 @@ Yes:: } } -No:: +No: + +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; @@ -473,7 +521,9 @@ Additionally there should be a single space between the control structures conditional, as well as a single space between the conditional parenthetic block and the opening brace. -Yes:: +Yes: + +.. code-block:: solidity if (...) { ... @@ -483,7 +533,9 @@ Yes:: ... } -No:: +No: + +.. code-block:: solidity if (...) { @@ -499,12 +551,16 @@ No:: For control structures whose body contains a single statement, omitting the braces is ok *if* the statement is contained on a single line. -Yes:: +Yes: + +.. code-block:: solidity if (x < 10) x += 1; -No:: +No: + +.. code-block:: solidity if (x < 10) someArray.push(Coin({ @@ -516,7 +572,9 @@ For ``if`` blocks which have an ``else`` or ``else if`` clause, the ``else`` sho placed on the same line as the ``if``'s closing brace. This is an exception compared to the rules of other block-like structures. -Yes:: +Yes: + +.. code-block:: solidity if (x < 3) { x += 1; @@ -532,7 +590,9 @@ Yes:: else x -= 1; -No:: +No: + +.. code-block:: solidity if (x < 3) { x += 1; @@ -552,7 +612,9 @@ declaration. The opening brace should be preceded by a single space. -Yes:: +Yes: + +.. code-block:: solidity function increment(uint x) public pure returns (uint) { return x + 1; @@ -562,7 +624,9 @@ Yes:: return x + 1; } -No:: +No: + +.. code-block:: solidity function increment(uint x) public pure returns (uint) { @@ -588,7 +652,9 @@ The modifier order for a function should be: 4. Override 5. Custom modifiers -Yes:: +Yes: + +.. code-block:: solidity function balance(uint from) public view override returns (uint) { return balanceOf[from]; @@ -598,7 +664,9 @@ Yes:: selfdestruct(owner); } -No:: +No: + +.. code-block:: solidity function balance(uint from) public override view returns (uint) { return balanceOf[from]; @@ -613,7 +681,9 @@ it's own line at the same indentation level as the function body. The closing parenthesis and opening bracket should be placed on their own line as well at the same indentation level as the function declaration. -Yes:: +Yes: + +.. code-block:: solidity function thisFunctionHasLotsOfArguments( address a, @@ -628,7 +698,9 @@ Yes:: doSomething(); } -No:: +No: + +.. code-block:: solidity function thisFunctionHasLotsOfArguments(address a, address b, address c, address d, address e, address f) public { @@ -657,7 +729,9 @@ No:: If a long function declaration has modifiers, then each modifier should be dropped to its own line. -Yes:: +Yes: + +.. code-block:: solidity function thisFunctionNameIsReallyLong(address x, address y, address z) public @@ -681,7 +755,9 @@ Yes:: doSomething(); } -No:: +No: + +.. code-block:: solidity function thisFunctionNameIsReallyLong(address x, address y, address z) public @@ -707,7 +783,9 @@ No:: Multiline output parameters and return statements should follow the same style recommended for wrapping long lines found in the :ref:`Maximum Line Length ` section. -Yes:: +Yes: + +.. code-block:: solidity function thisFunctionNameIsReallyLong( address a, @@ -730,7 +808,9 @@ Yes:: ); } -No:: +No: + +.. code-block:: solidity function thisFunctionNameIsReallyLong( address a, @@ -753,7 +833,9 @@ For constructor functions on inherited contracts whose bases require arguments, it is recommended to drop the base constructors onto new lines in the same manner as modifiers if the function declaration is long or hard to read. -Yes:: +Yes: + +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; @@ -784,7 +866,9 @@ Yes:: } } -No:: +No: + +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; @@ -834,7 +918,9 @@ No:: When declaring short functions with a single statement, it is permissible to do it on a single line. -Permissible:: +Permissible: + +.. code-block:: solidity function shortFunction() public { doSomething(); } @@ -849,14 +935,18 @@ In variable declarations, do not separate the keyword ``mapping`` from its type by a space. Do not separate any nested ``mapping`` keyword from its type by whitespace. -Yes:: +Yes: + +.. code-block:: solidity mapping(uint => uint) map; mapping(address => bool) registeredAddresses; mapping(uint => mapping(bool => Data[])) public data; mapping(uint => mapping(uint => s)) data; -No:: +No: + +.. code-block:: solidity mapping (uint => uint) map; mapping( address => bool ) registeredAddresses; @@ -869,11 +959,15 @@ Variable Declarations Declarations of array variables should not have a space between the type and the brackets. -Yes:: +Yes: + +.. code-block:: solidity uint[] x; -No:: +No: + +.. code-block:: solidity uint [] x; @@ -883,26 +977,34 @@ Other Recommendations * Strings should be quoted with double-quotes instead of single-quotes. -Yes:: +Yes: + +.. code-block:: solidity str = "foo"; str = "Hamlet says, 'To be or not to be...'"; -No:: +No: + +.. code-block:: solidity str = 'bar'; str = '"Be yourself; everyone else is already taken." -Oscar Wilde'; * Surround operators with a single space on either side. -Yes:: +Yes: + +.. code-block:: solidity x = 3; x = 100 / 10; x += 3 + 4; x |= y && z; -No:: +No: + +.. code-block:: solidity x=3; x = 100/10; @@ -914,13 +1016,17 @@ No:: improved readability for complex statement. You should always use the same amount of whitespace on either side of an operator: -Yes:: +Yes: + +.. code-block:: solidity x = 2**3 + 5; x = 2*y + 3*z; x = (a+b) * (a-b); -No:: +No: + +.. code-block:: solidity x = 2** 3 + 5; x = y+z; @@ -1005,7 +1111,9 @@ Contract and Library Names As shown in the example below, if the contract name is ``Congress`` and the library name is ``Owned``, then their associated filenames should be ``Congress.sol`` and ``Owned.sol``. -Yes:: +Yes: + +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; @@ -1028,7 +1136,9 @@ Yes:: } } -and in ``Congress.sol``:: +and in ``Congress.sol``: + +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; @@ -1040,7 +1150,9 @@ and in ``Congress.sol``:: //... } -No:: +No: + +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; @@ -1063,7 +1175,9 @@ No:: } } -and in ``Congress.sol``:: +and in ``Congress.sol``: + +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.7.0; @@ -1147,7 +1261,9 @@ triple slash (``///``) or a double asterisk block (``/** ... */``) and they should be used directly above function declarations or statements. For example, the contract from :ref:`a simple smart contract ` with the comments -added looks like the one below:: +added looks like the one below: + +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0; diff --git a/docs/types/conversion.rst b/docs/types/conversion.rst index 38e0ca6f1..7e266d1bd 100644 --- a/docs/types/conversion.rst +++ b/docs/types/conversion.rst @@ -32,7 +32,7 @@ in the ``uint16`` type. The resulting type of the expression ``y + z`` is ``uint Because it is assigned to a variable of type ``uint32`` another implicit conversion is performed after the addition. -:: +.. code-block:: solidity uint8 y; uint16 z; @@ -50,7 +50,7 @@ result is what you want and expect! Take the following example that converts a negative ``int`` to a ``uint``: -:: +.. code-block:: solidity int y = -3; uint x = uint(y); @@ -59,13 +59,17 @@ At the end of this code snippet, ``x`` will have the value ``0xfffff..fd`` (64 h characters), which is -3 in the two's complement representation of 256 bits. If an integer is explicitly converted to a smaller type, higher-order bits are -cut off:: +cut off: + +.. code-block:: solidity uint32 a = 0x12345678; uint16 b = uint16(a); // b will be 0x5678 now If an integer is explicitly converted to a larger type, it is padded on the left (i.e., at the higher order end). -The result of the conversion will compare equal to the original integer:: +The result of the conversion will compare equal to the original integer: + +.. code-block:: solidity uint16 a = 0x1234; uint32 b = uint32(a); // b will be 0x00001234 now @@ -73,14 +77,18 @@ The result of the conversion will compare equal to the original integer:: Fixed-size bytes types behave differently during conversions. They can be thought of as sequences of individual bytes and converting to a smaller type will cut off the -sequence:: +sequence: + +.. code-block:: solidity bytes2 a = 0x1234; bytes1 b = bytes1(a); // b will be 0x12 If a fixed-size bytes type is explicitly converted to a larger type, it is padded on the right. Accessing the byte at a fixed index will result in the same value before and -after the conversion (if the index is still in range):: +after the conversion (if the index is still in range): + +.. code-block:: solidity bytes2 a = 0x1234; bytes4 b = bytes4(a); // b will be 0x12340000 @@ -91,7 +99,9 @@ Since integers and fixed-size byte arrays behave differently when truncating or padding, explicit conversions between integers and fixed-size byte arrays are only allowed, if both have the same size. If you want to convert between integers and fixed-size byte arrays of different size, you have to use intermediate conversions that make the desired truncation and padding -rules explicit:: +rules explicit: + +.. code-block:: solidity bytes2 a = 0x1234; uint32 b = uint16(a); // b will be 0x00001234 @@ -103,7 +113,7 @@ rules explicit:: In case the array is longer than the target fixed bytes type, truncation at the end will happen. If the array is shorter than the target type, it will be padded with zeros at the end. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.5; @@ -129,7 +139,9 @@ Integer Types ------------- Decimal and hexadecimal number literals can be implicitly converted to any integer type -that is large enough to represent it without truncation:: +that is large enough to represent it without truncation: + +.. code-block:: solidity uint8 a = 12; // fine uint32 b = 1234; // fine @@ -146,7 +158,9 @@ Fixed-Size Byte Arrays Decimal number literals cannot be implicitly converted to fixed-size byte arrays. Hexadecimal number literals can be, but only if the number of hex digits exactly fits the size of the bytes type. As an exception both decimal and hexadecimal literals which have a value of zero can be -converted to any fixed-size bytes type:: +converted to any fixed-size bytes type: + +.. code-block:: solidity bytes2 a = 54321; // not allowed bytes2 b = 0x12; // not allowed @@ -157,7 +171,9 @@ converted to any fixed-size bytes type:: bytes4 g = 0x0; // fine String literals and hex string literals can be implicitly converted to fixed-size byte arrays, -if their number of characters matches the size of the bytes type:: +if their number of characters matches the size of the bytes type: + +.. code-block:: solidity bytes2 a = hex"1234"; // fine bytes2 b = "xy"; // fine diff --git a/docs/types/mapping-types.rst b/docs/types/mapping-types.rst index edf04aed2..29a00a196 100644 --- a/docs/types/mapping-types.rst +++ b/docs/types/mapping-types.rst @@ -40,7 +40,7 @@ an Ethereum address to an unsigned integer value. As ``uint`` is a value type, t returns a value that matches the type, which you can see in the ``MappingUser`` contract that returns the value at the specified address. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; @@ -66,7 +66,7 @@ The example below is a simplified version of an ``_allowances`` is an example of a mapping type inside another mapping type. The example below uses ``_allowances`` to record the amount someone else is allowed to withdraw from your account. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.22 <0.9.0; @@ -121,7 +121,7 @@ top of them and iterate over that. For example, the code below implements an ``IterableMapping`` library that the ``User`` contract then adds data too, and the ``sum`` function iterates over to sum all the values. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.8 <0.9.0; diff --git a/docs/types/operators.rst b/docs/types/operators.rst index 883f8d4b7..733dc806d 100644 --- a/docs/types/operators.rst +++ b/docs/types/operators.rst @@ -40,7 +40,7 @@ This distinction is visible when ``a`` is reference variable: It will only reset ``a`` itself, not the value it referred to previously. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; diff --git a/docs/types/reference-types.rst b/docs/types/reference-types.rst index 8d21221b4..b6a36fc6e 100644 --- a/docs/types/reference-types.rst +++ b/docs/types/reference-types.rst @@ -60,7 +60,7 @@ Data locations are not only relevant for persistency of data, but also for the s variables of storage struct type, even if the local variable itself is just a reference. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.5.0 <0.9.0; @@ -114,7 +114,7 @@ Indices are zero-based, and access is in the opposite direction of the declaration. For example, if you have a variable ``uint[][5] memory x``, you access the -second ``uint`` in the third dynamic array using ``x[2][1]``, and to access the +seventh ``uint`` in the third dynamic array using ``x[2][6]``, and to access the third dynamic array, use ``x[2]``. Again, if you have an array ``T[5] a`` for a type ``T`` that can also be an array, then ``a[2]`` always has type ``T``. @@ -171,7 +171,7 @@ You can concatenate a variable number of ``bytes`` or ``bytes1 ... bytes32`` usi The function returns a single ``bytes memory`` array that contains the contents of the arguments without padding. If you want to use string parameters or other types, you need to convert them to ``bytes`` or ``bytes1``/.../``bytes32`` first. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.4; @@ -200,7 +200,7 @@ or create a new memory array and copy every element. As all variables in Solidity, the elements of newly allocated arrays are always initialized with the :ref:`default value`. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0; @@ -239,7 +239,7 @@ In the example below, the type of ``[1, 2, 3]`` is you want the result to be a ``uint[3] memory`` type, you need to convert the first element to ``uint``. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0; @@ -261,7 +261,7 @@ Since fixed-size memory arrays of different type cannot be converted into each o (even if the base types can), you always have to specify a common base type explicitly if you want to use two-dimensional array literals: -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0; @@ -278,7 +278,7 @@ if you want to use two-dimensional array literals: Fixed size memory arrays cannot be assigned to dynamically-sized memory arrays, i.e. the following is not possible: -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; @@ -298,7 +298,7 @@ complications because of how arrays are passed in the ABI. If you want to initialize dynamically-sized arrays, you have to assign the individual elements: -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0; @@ -356,7 +356,7 @@ Array Members that return dynamic arrays, make sure to use an EVM that is set to Byzantium mode. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.0 <0.9.0; @@ -489,7 +489,7 @@ they only exist in intermediate expressions. Array slices are useful to ABI-decode secondary data passed in function parameters: -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >0.8.4 <0.9.0; @@ -528,7 +528,7 @@ Structs Solidity provides a way to define new types in the form of structs, which is shown in the following example: -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.0 <0.9.0; diff --git a/docs/types/value-types.rst b/docs/types/value-types.rst index ee94ed60a..d6a30d106 100644 --- a/docs/types/value-types.rst +++ b/docs/types/value-types.rst @@ -66,16 +66,23 @@ Shifts ^^^^^^ The result of a shift operation has the type of the left operand, truncating the result to match the type. -Right operand must be unsigned type. Trying to shift by signed type will produce a compilation error. +The right operand must be of unsigned type, trying to shift by an signed type will produce a compilation error. -- For positive and negative ``x`` values, ``x << y`` is equivalent to ``x * 2**y``. -- For positive ``x`` values, ``x >> y`` is equivalent to ``x / 2**y``. -- For negative ``x`` values, ``x >> y`` is equivalent to ``(x + 1) / 2**y - 1`` (which is the same as dividing ``x`` by ``2**y`` while rounding down towards negative infinity). +Shifts can be "simulated" using multiplication by powers of two in the following way. Note that the truncation +to the type of the left operand is always performed at the end, but not mentioned explicitly. + +- ``x << y`` is equivalent to the mathematical expression ``x * 2**y``. +- ``x >> y`` is equivalent to the mathematical expression ``x / 2**y``, rounded towards negative infinity. .. warning:: - Before version ``0.5.0`` a right shift ``x >> y`` for negative ``x`` was equivalent to ``x / 2**y``, + Before version ``0.5.0`` a right shift ``x >> y`` for negative ``x`` was equivalent to + the mathematical expression ``x / 2**y`` rounded towards zero, i.e., right shifts used rounding up (towards zero) instead of rounding down (towards negative infinity). +.. note:: + Overflow checks are never performed for shift operations as they are done for arithmetic operations. + Instead, the result is always truncated. + Addition, Subtraction and Multiplication ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -231,7 +238,7 @@ For a quick reference of all members of address, see :ref:`address_related`. It is possible to query the balance of an address using the property ``balance`` and to send Ether (in units of wei) to a payable address using the ``transfer`` function: -:: +.. code-block:: solidity address payable x = address(0x123); address myAddress = address(this); @@ -265,7 +272,9 @@ return the success condition (as a ``bool``) and the returned data The functions ``abi.encode``, ``abi.encodePacked``, ``abi.encodeWithSelector`` and ``abi.encodeWithSignature`` can be used to encode structured data. -Example:: +Example: + +.. code-block:: solidity bytes memory payload = abi.encodeWithSignature("register(string)", "MyName"); (bool success, bytes memory returnData) = address(nameReg).call(payload); @@ -284,15 +293,21 @@ Example:: arbitrary arguments and would also handle a first argument of type ``bytes4`` differently. These edge cases were removed in version 0.5.0. -It is possible to adjust the supplied gas with the ``gas`` modifier:: +It is possible to adjust the supplied gas with the ``gas`` modifier: + +.. code-block:: solidity address(nameReg).call{gas: 1000000}(abi.encodeWithSignature("register(string)", "MyName")); -Similarly, the supplied Ether value can be controlled too:: +Similarly, the supplied Ether value can be controlled too: + +.. code-block:: solidity address(nameReg).call{value: 1 ether}(abi.encodeWithSignature("register(string)", "MyName")); -Lastly, these modifiers can be combined. Their order does not matter:: +Lastly, these modifiers can be combined. Their order does not matter: + +.. code-block:: solidity address(nameReg).call{gas: 1000000, value: 1 ether}(abi.encodeWithSignature("register(string)", "MyName")); @@ -305,8 +320,8 @@ Since byzantium ``staticcall`` can be used as well. This is basically the same a All three functions ``call``, ``delegatecall`` and ``staticcall`` are very low-level functions and should only be used as a *last resort* as they break the type-safety of Solidity. -The ``gas`` option is available on all three methods, while the ``value`` option is not -supported for ``delegatecall``. +The ``gas`` option is available on all three methods, while the ``value`` option is only available +on ``call``. .. note:: It is best to avoid relying on hardcoded gas values in your smart contract code, @@ -476,7 +491,7 @@ regardless of the type of the right (exponent) operand. for the type of ``2.5`` and ``uint128``, the Solidity compiler does not accept this code. -:: +.. code-block:: solidity uint128 a = 1; uint128 b = 2.5 + a + 0.5; @@ -529,7 +544,7 @@ Unicode Literals While regular string literals can only contain ASCII, Unicode literals – prefixed with the keyword ``unicode`` – can contain any valid UTF-8 sequence. They also support the very same escape sequences as regular string literals. -:: +.. code-block:: solidity string memory a = unicode"Hello 😃"; @@ -567,7 +582,7 @@ The data representation is the same as for enums in C: The options are represent subsequent unsigned integer values starting from ``0``. -:: +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0; @@ -619,7 +634,9 @@ contract internally. External functions consist of an address and a function signature and they can be passed via and returned from external function calls. -Function types are notated as follows:: +Function types are notated as follows: + +.. code-block:: solidity function () {internal|external} [pure|view|payable] [returns ()] @@ -688,7 +705,9 @@ External (or public) functions have the following members: respectively. See :ref:`External Function Calls ` for more information. -Example that shows how to use the members:: +Example that shows how to use the members: + +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.4 <0.9.0; @@ -704,7 +723,9 @@ Example that shows how to use the members:: } } -Example that shows how to use internal function types:: +Example that shows how to use internal function types: + +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0; @@ -762,7 +783,9 @@ Example that shows how to use internal function types:: } } -Another example that uses external function types:: +Another example that uses external function types: + +.. code-block:: solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.22 <0.9.0; diff --git a/docs/units-and-global-variables.rst b/docs/units-and-global-variables.rst index 1cad59e42..2e912ae84 100644 --- a/docs/units-and-global-variables.rst +++ b/docs/units-and-global-variables.rst @@ -9,7 +9,7 @@ Ether Units A literal number can take a suffix of ``wei``, ``gwei`` or ``ether`` to specify a subdenomination of Ether, where Ether numbers without a postfix are assumed to be Wei. -:: +.. code-block:: solidity assert(1 wei == 1); assert(1 gwei == 1e9); @@ -45,7 +45,9 @@ library has to be updated by an external oracle. The suffix ``years`` has been removed in version 0.5.0 due to the reasons above. These suffixes cannot be applied to variables. For example, if you want to -interpret a function parameter in days, you can in the following way:: +interpret a function parameter in days, you can in the following way: + +.. code-block:: solidity function f(uint start, uint daysAfter) public { if (block.timestamp >= start + daysAfter * 1 days) { @@ -68,7 +70,7 @@ or are general-use utility functions. Block and Transaction Properties -------------------------------- -- ``blockhash(uint blockNumber) returns (bytes32)``: hash of the given block - only works for 256 most recent, excluding current, blocks +- ``blockhash(uint blockNumber) returns (bytes32)``: hash of the given block when ``blocknumber`` is one of the 256 most recent blocks; otherwise returns zero - ``block.chainid`` (``uint``): current chain id - ``block.coinbase`` (``address payable``): current block miner's address - ``block.difficulty`` (``uint``): current block difficulty diff --git a/docs/using-the-compiler.rst b/docs/using-the-compiler.rst index 1ccebf359..a023a8877 100644 --- a/docs/using-the-compiler.rst +++ b/docs/using-the-compiler.rst @@ -2,7 +2,7 @@ Using the Compiler ****************** -.. index:: ! commandline compiler, compiler;commandline, ! solc, ! linker +.. index:: ! commandline compiler, compiler;commandline, ! solc .. _commandline-compiler: @@ -33,11 +33,13 @@ This parameter has effects on the following (this might change in the future): - the size of the binary search in the function dispatch routine - the way constants like large numbers or strings are stored -Path Remapping --------------- +.. index:: allowed paths, --allow-paths, base path, --base-path + +Base Path and Import Remapping +------------------------------ The commandline compiler will automatically read imported files from the filesystem, but -it is also possible to provide path redirects using ``prefix=path`` in the following way: +it is also possible to provide :ref:`path redirects ` using ``prefix=path`` in the following way: :: @@ -49,19 +51,24 @@ This essentially instructs the compiler to search for anything starting with the remapping targets and outside of the directories where explicitly specified source files reside, so things like ``import "/etc/passwd";`` only work if you add ``/=/`` as a remapping. -An empty remapping prefix is not allowed. - -If there are multiple matches due to remappings, the one with the longest common prefix is selected. - -When accessing the filesystem to search for imports, all paths are treated as if they were fully qualified paths. -This behaviour can be customized by adding the command line option ``--base-path`` with a path to be prepended -before each filesystem access for imports is performed. Furthermore, the part added via ``--base-path`` -will not appear in the contract metadata. - -For security reasons the compiler has restrictions what directories it can access. Paths (and their subdirectories) of source files specified on the commandline and paths defined by remappings are allowed for import statements, but everything else is rejected. Additional paths (and their subdirectories) can be allowed via the ``--allow-paths /sample/path,/another/sample/path`` switch. +When accessing the filesystem to search for imports, :ref:`paths that do not start with ./ +or ../ ` are treated as relative to the directory specified using +``--base-path`` option (or the current working directory if base path is not specified). +Furthermore, the part added via ``--base-path`` will not appear in the contract metadata. +For security reasons the compiler has restrictions on what directories it can access. +Directories of source files specified on the command line and target paths of +remappings are automatically allowed to be accessed by the file reader, but everything +else is rejected by default. +Additional paths (and their subdirectories) can be allowed via the +``--allow-paths /sample/path,/another/sample/path`` switch. Everything inside the path specified via ``--base-path`` is always allowed. +The above is only a simplification of how the compiler handles import paths. +For a detailed explanation with examples and discussion of corner cases please refer to the section on +:ref:`path resolution `. + +.. index:: ! linker, ! --link, ! --libraries .. _library-linking: Library Linking @@ -79,6 +86,8 @@ Either add ``--libraries "file.sol:Math=0x12345678901234567890123456789012345678 .. note:: Starting Solidity 0.8.1 accepts ``=`` as separator between library and address, and ``:`` as a separator is deprecated. It will be removed in the future. Currently ``--libraries "file.sol:Math:0x1234567890123456789012345678901234567890 file.sol:Heap:0xabCD567890123456789012345678901234567890"`` will work too. +.. index:: --standard-json, --base-path + If ``solc`` is called with the option ``--standard-json``, it will expect a JSON input (as explained below) on the standard input, and return a JSON output on the standard output. This is the recommended interface for more complex and especially automated uses. The process will always terminate in a "success" state and report any errors via the JSON output. The option ``--base-path`` is also processed in standard-json mode. @@ -158,11 +167,15 @@ at each version. Backward compatibility is not guaranteed between each version. - Shifting operators use shifting opcodes and thus need less gas. - ``petersburg`` - The compiler behaves the same way as with constantinople. -- ``istanbul`` (**default**) +- ``istanbul`` - Opcodes ``chainid`` and ``selfbalance`` are available in assembly. -- ``berlin`` (**experimental**) +- ``berlin`` (**default**) + - Gas costs for ``SLOAD``, ``*CALL``, ``BALANCE``, ``EXT*`` and ``SELFDESTRUCT`` increased. The + compiler assumes cold gas costs for such operations. This is relevant for gas estimation and + the optimizer. +.. index:: ! standard JSON, ! --standard-json .. _compiler-api: Compiler Input and Output JSON Description @@ -233,7 +246,10 @@ Input Description "remappings": [ ":g=/dir" ], // Optional: Optimizer settings "optimizer": { - // disabled by default + // Disabled by default. + // NOTE: enabled=false still leaves some optimizations on. See comments below. + // WARNING: Before version 0.8.6 omitting the 'enabled' key was not equivalent to setting + // it to false and would actually disable all the optimizations. "enabled": true, // Optimize for how many times you intend to run the code. // Lower values will optimize more for initial deployment cost, higher @@ -344,6 +360,7 @@ Input Description // storageLayout - Slots, offsets and types of the contract's state variables. // evm.assembly - New assembly format // evm.legacyAssembly - Old-style assembly format in JSON + // evm.bytecode.functionDebugData - Debugging information at function level // evm.bytecode.object - Bytecode object // evm.bytecode.opcodes - Opcodes list // evm.bytecode.sourceMap - Source mapping (useful for debugging) @@ -476,6 +493,17 @@ Output Description "legacyAssembly": {}, // Bytecode and related details. "bytecode": { + // Debugging data at the level of functions. + "functionDebugData": { + // Now follows a set of functions including compiler-internal and + // user-defined function. The set does not have to be complete. + "@mint_13": { // Internal name of the function + "entryPoint": 128, // Byte offset into the bytecode where the function starts (optional) + "id": 13, // AST ID of the function definition or null for compiler-internal functions (optional) + "parameterSlots": 2, // Number of EVM stack slots for the function parameters (optional) + "returnSlots": 1 // Number of EVM stack slots for the return values (optional) + } + }, // The bytecode as a hex string. "object": "00fe", // Opcodes list (string) diff --git a/docs/yul.rst b/docs/yul.rst index 2c77e4402..55e8b824f 100644 --- a/docs/yul.rst +++ b/docs/yul.rst @@ -1076,6 +1076,23 @@ regular strings in native encoding. For code, Above, ``Block`` refers to ``Block`` in the Yul code grammar explained in the previous chapter. +.. note:: + + Data objects or sub-objects whose names contain a ``.`` can be defined + but it is not possible to access them through ``datasize``, + ``dataoffset`` or ``datacopy`` because ``.`` is used as a separator + to access objects inside another object. + +.. note:: + + The data object called ``".metadata"`` has a special meaning: + It cannot be accessed from code and is always appended to the very end of the + bytecode, regardless of its position in the object. + + Other data objects with special significance might be added in the + future, but their names will always start with a ``.``. + + An example Yul Object is shown below: .. code-block:: yul @@ -1155,9 +1172,8 @@ Yul Optimizer The Yul optimizer operates on Yul code and uses the same language for input, output and intermediate states. This allows for easy debugging and verification of the optimizer. -Please see the -`documentation in the source code `_ -for more details about its internals. +Please refer to the general :ref:`optimizer documentation ` +for more details about the different optimization stages and how to use the optimizer. If you want to use Solidity in stand-alone Yul mode, you activate the optimizer using ``--optimize``: @@ -1167,7 +1183,7 @@ If you want to use Solidity in stand-alone Yul mode, you activate the optimizer In Solidity mode, the Yul optimizer is activated together with the regular optimizer. -Optimization step sequence +Optimization Step Sequence -------------------------- By default the Yul optimizer applies its predefined sequence of optimization steps to the generated assembly. diff --git a/libevmasm/Assembly.cpp b/libevmasm/Assembly.cpp index 50be995a8..f63f3ede6 100644 --- a/libevmasm/Assembly.cpp +++ b/libevmasm/Assembly.cpp @@ -348,12 +348,18 @@ Json::Value Assembly::assemblyJSON(map const& _sourceIndices) return root; } -AssemblyItem Assembly::namedTag(string const& _name) +AssemblyItem Assembly::namedTag(string const& _name, size_t _params, size_t _returns, optional _sourceID) { assertThrow(!_name.empty(), AssemblyException, "Empty named tag."); - if (!m_namedTags.count(_name)) - m_namedTags[_name] = static_cast(newTag().data()); - return AssemblyItem{Tag, m_namedTags.at(_name)}; + if (m_namedTags.count(_name)) + { + assertThrow(m_namedTags.at(_name).params == _params, AssemblyException, ""); + assertThrow(m_namedTags.at(_name).returns == _returns, AssemblyException, ""); + assertThrow(m_namedTags.at(_name).sourceID == _sourceID, AssemblyException, ""); + } + else + m_namedTags[_name] = {static_cast(newTag().data()), _sourceID, _params, _returns}; + return AssemblyItem{Tag, m_namedTags.at(_name).id}; } AssemblyItem Assembly::newPushLibraryAddress(string const& _identifier) @@ -722,13 +728,16 @@ LinkerObject const& Assembly::assemble() const ret.bytecode.resize(ret.bytecode.size() + 20); break; case Tag: + { assertThrow(i.data() != 0, AssemblyException, "Invalid tag position."); assertThrow(i.splitForeignPushTag().first == numeric_limits::max(), AssemblyException, "Foreign tag."); + size_t tagId = static_cast(i.data()); assertThrow(ret.bytecode.size() < 0xffffffffL, AssemblyException, "Tag too large."); - assertThrow(m_tagPositionsInBytecode[static_cast(i.data())] == numeric_limits::max(), AssemblyException, "Duplicate tag position."); - m_tagPositionsInBytecode[static_cast(i.data())] = ret.bytecode.size(); + assertThrow(m_tagPositionsInBytecode[tagId] == numeric_limits::max(), AssemblyException, "Duplicate tag position."); + m_tagPositionsInBytecode[tagId] = ret.bytecode.size(); ret.bytecode.push_back(static_cast(Instruction::JUMPDEST)); break; + } default: assertThrow(false, InvalidOpcode, "Unexpected opcode while assembling."); } @@ -770,6 +779,17 @@ LinkerObject const& Assembly::assemble() const bytesRef r(ret.bytecode.data() + i.first, bytesPerTag); toBigEndian(pos, r); } + for (auto const& [name, tagInfo]: m_namedTags) + { + size_t position = m_tagPositionsInBytecode.at(tagInfo.id); + ret.functionDebugData[name] = { + position == numeric_limits::max() ? nullopt : optional{position}, + tagInfo.sourceID, + tagInfo.params, + tagInfo.returns + }; + } + for (auto const& dataItem: m_data) { auto references = dataRef.equal_range(dataItem.first); diff --git a/libevmasm/Assembly.h b/libevmasm/Assembly.h index e16023ad9..4a0779ef7 100644 --- a/libevmasm/Assembly.h +++ b/libevmasm/Assembly.h @@ -30,11 +30,14 @@ #include #include +#include + #include #include #include #include +#include namespace solidity::evmasm { @@ -49,7 +52,7 @@ public: AssemblyItem newTag() { assertThrow(m_usedTags < 0xffffffff, AssemblyException, ""); return AssemblyItem(Tag, m_usedTags++); } AssemblyItem newPushTag() { assertThrow(m_usedTags < 0xffffffff, AssemblyException, ""); return AssemblyItem(PushTag, m_usedTags++); } /// Returns a tag identified by the given name. Creates it if it does not yet exist. - AssemblyItem namedTag(std::string const& _name); + AssemblyItem namedTag(std::string const& _name, size_t _params, size_t _returns, std::optional _sourceID); AssemblyItem newData(bytes const& _data) { util::h256 h(util::keccak256(util::asString(_data))); m_data[h] = _data; return AssemblyItem(PushData, h); } bytes const& data(util::h256 const& _i) const { return m_data.at(_i); } AssemblyItem newSub(AssemblyPointer const& _sub) { m_subs.push_back(_sub); return AssemblyItem(PushSub, m_subs.size() - 1); } @@ -91,7 +94,7 @@ public: void pushSubroutineOffset(size_t _subRoutine) { append(AssemblyItem(PushSub, _subRoutine)); } /// Appends @a _data literally to the very end of the bytecode. - void appendAuxiliaryDataToEnd(bytes const& _data) { m_auxiliaryData += _data; } + void appendToAuxiliaryData(bytes const& _data) { m_auxiliaryData += _data; } /// Returns the assembly items. AssemblyItems const& items() const { return m_items; } @@ -123,7 +126,7 @@ public: langutil::EVMVersion evmVersion; /// This specifies an estimate on how often each opcode in this assembly will be executed, /// i.e. use a small value to optimise for size and a large value to optimise for runtime gas usage. - size_t expectedExecutionsPerDeployment = 200; + size_t expectedExecutionsPerDeployment = frontend::OptimiserSettings{}.expectedExecutionsPerDeployment; }; /// Modify and return the current assembly such that creation and execution gas usage @@ -184,7 +187,16 @@ private: protected: /// 0 is reserved for exception unsigned m_usedTags = 1; - std::map m_namedTags; + + struct NamedTagInfo + { + size_t id; + std::optional sourceID; + size_t params; + size_t returns; + }; + + std::map m_namedTags; AssemblyItems m_items; std::map m_data; /// Data that is appended to the very end of the contract. diff --git a/libevmasm/ExpressionClasses.cpp b/libevmasm/ExpressionClasses.cpp index e2b39a7be..8df523842 100644 --- a/libevmasm/ExpressionClasses.cpp +++ b/libevmasm/ExpressionClasses.cpp @@ -29,7 +29,6 @@ #include #include -#include using namespace std; using namespace solidity; diff --git a/libevmasm/GasMeter.cpp b/libevmasm/GasMeter.cpp index 089a371c6..57a898f65 100644 --- a/libevmasm/GasMeter.cpp +++ b/libevmasm/GasMeter.cpp @@ -71,9 +71,9 @@ GasMeter::GasConsumption GasMeter::estimateMax(AssemblyItem const& _item, bool _ m_state->storageContent().count(slot) && classes.knownNonZero(m_state->storageContent().at(slot)) )) - gas = GasCosts::sstoreResetGas; //@todo take refunds into account + gas = GasCosts::totalSstoreResetGas(m_evmVersion); //@todo take refunds into account else - gas = GasCosts::sstoreSetGas; + gas = GasCosts::totalSstoreSetGas(m_evmVersion); break; } case Instruction::SLOAD: diff --git a/libevmasm/GasMeter.h b/libevmasm/GasMeter.h index ef7bc7073..dc5fca5cc 100644 --- a/libevmasm/GasMeter.h +++ b/libevmasm/GasMeter.h @@ -18,6 +18,11 @@ /** @file GasMeter.cpp * @author Christian * @date 2015 + * + * Utilities for tracking gas costs. + * + * With respect to EIP-2929, we do not track warm accounts or storage slots and they are always + * charged the worst-case, i.e., cold-access. */ #pragma once @@ -47,19 +52,6 @@ namespace GasCosts static unsigned const tier5Gas = 10; static unsigned const tier6Gas = 20; static unsigned const tier7Gas = 0; - inline unsigned extCodeGas(langutil::EVMVersion _evmVersion) - { - return _evmVersion >= langutil::EVMVersion::tangerineWhistle() ? 700 : 20; - } - inline unsigned balanceGas(langutil::EVMVersion _evmVersion) - { - if (_evmVersion >= langutil::EVMVersion::istanbul()) - return 700; - else if (_evmVersion >= langutil::EVMVersion::tangerineWhistle()) - return 400; - else - return 20; - } static unsigned const expGas = 10; inline unsigned expByteGas(langutil::EVMVersion _evmVersion) { @@ -67,18 +59,65 @@ namespace GasCosts } static unsigned const keccak256Gas = 30; static unsigned const keccak256WordGas = 6; + /// Corresponds to COLD_SLOAD_COST from EIP-2929 + static unsigned const coldSloadCost = 2100; + /// Corresponds to COLD_ACCOUNT_ACCESS_COST from EIP-2929 + static unsigned const coldAccountAccessCost = 2600; + /// Corresponds to WARM_STORAGE_READ_COST from EIP-2929 + static unsigned const warmStorageReadCost = 100; inline unsigned sloadGas(langutil::EVMVersion _evmVersion) { - if (_evmVersion >= langutil::EVMVersion::istanbul()) + if (_evmVersion >= langutil::EVMVersion::berlin()) + return coldSloadCost; + else if (_evmVersion >= langutil::EVMVersion::istanbul()) return 800; else if (_evmVersion >= langutil::EVMVersion::tangerineWhistle()) return 200; else return 50; } + /// Corresponds to SSTORE_SET_GAS static unsigned const sstoreSetGas = 20000; - static unsigned const sstoreResetGas = 5000; + /// Corresponds to SSTORE_RESET_GAS from EIP-2929 + static unsigned const sstoreResetGas = 5000 - coldSloadCost; static unsigned const sstoreRefundGas = 15000; + inline static unsigned totalSstoreSetGas(langutil::EVMVersion _evmVersion) + { + if (_evmVersion >= langutil::EVMVersion::berlin()) + return sstoreSetGas + coldSloadCost; + else + return sstoreSetGas; + } + /// Corresponds to SSTORE_RESET_GAS from EIP-2929 + /// For Berlin, the maximum is SSTORE_RESET_GAS + COLD_SLOAD_COST = 5000 + /// For previous versions, it's a fixed 5000 + inline unsigned totalSstoreResetGas(langutil::EVMVersion _evmVersion) + { + if (_evmVersion >= langutil::EVMVersion::berlin()) + return sstoreResetGas + coldSloadCost; + else + return 5000; + } + inline unsigned extCodeGas(langutil::EVMVersion _evmVersion) + { + if (_evmVersion >= langutil::EVMVersion::berlin()) + return coldAccountAccessCost; + else if (_evmVersion >= langutil::EVMVersion::tangerineWhistle()) + return 700; + else + return 20; + } + inline unsigned balanceGas(langutil::EVMVersion _evmVersion) + { + if (_evmVersion >= langutil::EVMVersion::berlin()) + return coldAccountAccessCost; + else if (_evmVersion >= langutil::EVMVersion::istanbul()) + return 700; + else if (_evmVersion >= langutil::EVMVersion::tangerineWhistle()) + return 400; + else + return 20; + } static unsigned const jumpdestGas = 1; static unsigned const logGas = 375; static unsigned const logDataGas = 8; @@ -86,14 +125,24 @@ namespace GasCosts static unsigned const createGas = 32000; inline unsigned callGas(langutil::EVMVersion _evmVersion) { - return _evmVersion >= langutil::EVMVersion::tangerineWhistle() ? 700 : 40; + if (_evmVersion >= langutil::EVMVersion::berlin()) + return coldAccountAccessCost; + else if (_evmVersion >= langutil::EVMVersion::tangerineWhistle()) + return 700; + else + return 40; } static unsigned const callStipend = 2300; static unsigned const callValueTransferGas = 9000; static unsigned const callNewAccountGas = 25000; inline unsigned selfdestructGas(langutil::EVMVersion _evmVersion) { - return _evmVersion >= langutil::EVMVersion::tangerineWhistle() ? 5000 : 0; + if (_evmVersion >= langutil::EVMVersion::berlin()) + return coldAccountAccessCost; + else if (_evmVersion >= langutil::EVMVersion::tangerineWhistle()) + return 5000; + else + return 0; } static unsigned const selfdestructRefundGas = 24000; static unsigned const memoryGas = 3; diff --git a/libevmasm/Inliner.h b/libevmasm/Inliner.h index ed68dc4ce..874472351 100644 --- a/libevmasm/Inliner.h +++ b/libevmasm/Inliner.h @@ -22,6 +22,7 @@ #pragma once #include +#include #include #include @@ -75,7 +76,7 @@ private: AssemblyItems& m_items; std::set const& m_tagsReferencedFromOutside; - size_t const m_runs = 200; + size_t const m_runs = Assembly::OptimiserSettings{}.expectedExecutionsPerDeployment; bool const m_isCreation = false; langutil::EVMVersion const m_evmVersion; }; diff --git a/libevmasm/Instruction.cpp b/libevmasm/Instruction.cpp index 5ccc4c542..e9f528d89 100644 --- a/libevmasm/Instruction.cpp +++ b/libevmasm/Instruction.cpp @@ -24,7 +24,6 @@ #include #include -#include #include using namespace std; diff --git a/libevmasm/LinkerObject.h b/libevmasm/LinkerObject.h index 03d01cc2c..e512dd7ab 100644 --- a/libevmasm/LinkerObject.h +++ b/libevmasm/LinkerObject.h @@ -45,6 +45,17 @@ struct LinkerObject /// to a list of offsets into the bytecode that refer to their values. std::map>> immutableReferences; + struct FunctionDebugData + { + std::optional bytecodeOffset; + std::optional sourceID; + size_t params = {}; + size_t returns = {}; + }; + + /// Bytecode offsets of named tags like function entry points. + std::map functionDebugData; + /// Appends the bytecode of @a _other and incorporates its link references. void append(LinkerObject const& _other); diff --git a/libevmasm/SimplificationRules.cpp b/libevmasm/SimplificationRules.cpp index d2f30c22e..a21ed4195 100644 --- a/libevmasm/SimplificationRules.cpp +++ b/libevmasm/SimplificationRules.cpp @@ -26,7 +26,6 @@ #include #include -#include #include #include diff --git a/liblangutil/EVMVersion.h b/liblangutil/EVMVersion.h index b34426c92..a3aeff3d0 100644 --- a/liblangutil/EVMVersion.h +++ b/liblangutil/EVMVersion.h @@ -99,7 +99,7 @@ private: EVMVersion(Version _version): m_version(_version) {} - Version m_version = Version::Istanbul; + Version m_version = Version::Berlin; }; } diff --git a/liblangutil/ErrorReporter.cpp b/liblangutil/ErrorReporter.cpp index 5703d33b8..728c9dad9 100644 --- a/liblangutil/ErrorReporter.cpp +++ b/liblangutil/ErrorReporter.cpp @@ -23,7 +23,6 @@ #include #include -#include #include using namespace std; diff --git a/liblangutil/ParserBase.cpp b/liblangutil/ParserBase.cpp index 089f606fa..91f67861a 100644 --- a/liblangutil/ParserBase.cpp +++ b/liblangutil/ParserBase.cpp @@ -89,7 +89,7 @@ void ParserBase::expectToken(Token _value, bool _advance) _advance = false; } if (_advance) - m_scanner->next(); + advance(); } void ParserBase::expectTokenOrConsumeUntil(Token _value, string const& _currentNodeName, bool _advance) @@ -102,7 +102,7 @@ void ParserBase::expectTokenOrConsumeUntil(Token _value, string const& _currentN SourceLocation errorLoc = currentLocation(); int startPosition = errorLoc.start; while (m_scanner->currentToken() != _value && m_scanner->currentToken() != Token::EOS) - m_scanner->next(); + advance(); string const expectedToken = ParserBase::tokenName(_value); if (m_scanner->currentToken() == Token::EOS) @@ -126,7 +126,7 @@ void ParserBase::expectTokenOrConsumeUntil(Token _value, string const& _currentN } if (_advance) - m_scanner->next(); + advance(); } void ParserBase::increaseRecursionDepth() diff --git a/liblangutil/ParserBase.h b/liblangutil/ParserBase.h index 475b08456..c3fb1056d 100644 --- a/liblangutil/ParserBase.h +++ b/liblangutil/ParserBase.h @@ -81,7 +81,7 @@ protected: Token peekNextToken() const; std::string tokenName(Token _token); std::string currentLiteral() const; - Token advance(); + virtual Token advance(); ///@} /// Increases the recursion depth and throws an exception if it is too deep. diff --git a/liblangutil/Scanner.cpp b/liblangutil/Scanner.cpp index 5a9f9fcce..5dbc2529c 100644 --- a/liblangutil/Scanner.cpp +++ b/liblangutil/Scanner.cpp @@ -62,7 +62,8 @@ using namespace std; -namespace solidity::langutil { +namespace solidity::langutil +{ string to_string(ScannerError _errorCode) { diff --git a/liblangutil/SourceLocation.h b/liblangutil/SourceLocation.h index 4b7122de3..eb858bd06 100644 --- a/liblangutil/SourceLocation.h +++ b/liblangutil/SourceLocation.h @@ -28,6 +28,7 @@ #include +#include #include #include diff --git a/liblangutil/SourceReferenceExtractor.cpp b/liblangutil/SourceReferenceExtractor.cpp index d362755ba..3eac873aa 100644 --- a/liblangutil/SourceReferenceExtractor.cpp +++ b/liblangutil/SourceReferenceExtractor.cpp @@ -21,7 +21,6 @@ #include #include -#include using namespace std; using namespace solidity; diff --git a/liblangutil/SourceReferenceFormatter.cpp b/liblangutil/SourceReferenceFormatter.cpp index 28b4e3416..cc2afc24b 100644 --- a/liblangutil/SourceReferenceFormatter.cpp +++ b/liblangutil/SourceReferenceFormatter.cpp @@ -20,7 +20,6 @@ */ #include -#include #include #include #include diff --git a/libsmtutil/CHCSmtLib2Interface.cpp b/libsmtutil/CHCSmtLib2Interface.cpp index bbbc63bfe..028c5efc2 100644 --- a/libsmtutil/CHCSmtLib2Interface.cpp +++ b/libsmtutil/CHCSmtLib2Interface.cpp @@ -23,6 +23,8 @@ #include #include +#include + #include #include #include @@ -53,8 +55,7 @@ void CHCSmtLib2Interface::reset() m_accumulatedOutput.clear(); m_variables.clear(); m_unhandledQueries.clear(); - if (m_queryTimeout) - write("(set-option :timeout " + to_string(*m_queryTimeout) + ")"); + m_sortNames.clear(); } void CHCSmtLib2Interface::registerRelation(Expression const& _expr) @@ -64,27 +65,25 @@ void CHCSmtLib2Interface::registerRelation(Expression const& _expr) if (!m_variables.count(_expr.name)) { auto fSort = dynamic_pointer_cast(_expr.sort); - string domain = m_smtlib2->toSmtLibSort(fSort->domain); + string domain = toSmtLibSort(fSort->domain); // Relations are predicates which have implicit codomain Bool. m_variables.insert(_expr.name); write( - "(declare-rel |" + + "(declare-fun |" + _expr.name + "| " + domain + - ")" + " Bool)" ); } } -void CHCSmtLib2Interface::addRule(Expression const& _expr, std::string const& _name) +void CHCSmtLib2Interface::addRule(Expression const& _expr, std::string const& /*_name*/) { write( - "(rule (! " + + "(assert\n(forall " + forall() + "\n" + m_smtlib2->toSExpr(_expr) + - " :named " + - _name + - "))" + "))\n\n" ); } @@ -92,22 +91,29 @@ pair CHCSmtLib2Interface::query(Expre { string accumulated{}; swap(m_accumulatedOutput, accumulated); - for (auto const& var: m_smtlib2->variables()) - declareVariable(var.first, var.second); + solAssert(m_smtlib2, ""); + writeHeader(); + for (auto const& decl: m_smtlib2->userSorts() | ranges::views::values) + write(decl); m_accumulatedOutput += accumulated; + string queryRule = "(assert\n(forall " + forall() + "\n" + + "(=> " + _block.name + " false)" + "))"; string response = querySolver( m_accumulatedOutput + - "\n(query " + _block.name + " :print-certificate true)" + queryRule + + "\n(check-sat)" ); + swap(m_accumulatedOutput, accumulated); CheckResult result; // TODO proper parsing - if (boost::starts_with(response, "sat\n")) - result = CheckResult::SATISFIABLE; - else if (boost::starts_with(response, "unsat\n")) + if (boost::starts_with(response, "sat")) result = CheckResult::UNSATISFIABLE; - else if (boost::starts_with(response, "unknown\n")) + else if (boost::starts_with(response, "unsat")) + result = CheckResult::SATISFIABLE; + else if (boost::starts_with(response, "unknown")) result = CheckResult::UNKNOWN; else result = CheckResult::ERROR; @@ -124,10 +130,46 @@ void CHCSmtLib2Interface::declareVariable(string const& _name, SortPointer const else if (!m_variables.count(_name)) { m_variables.insert(_name); - write("(declare-var |" + _name + "| " + m_smtlib2->toSmtLibSort(*_sort) + ')'); + write("(declare-var |" + _name + "| " + toSmtLibSort(*_sort) + ')'); } } +string CHCSmtLib2Interface::toSmtLibSort(Sort const& _sort) +{ + if (!m_sortNames.count(&_sort)) + m_sortNames[&_sort] = m_smtlib2->toSmtLibSort(_sort); + return m_sortNames.at(&_sort); +} + +string CHCSmtLib2Interface::toSmtLibSort(vector const& _sorts) +{ + string ssort("("); + for (auto const& sort: _sorts) + ssort += toSmtLibSort(*sort) + " "; + ssort += ")"; + return ssort; +} + +void CHCSmtLib2Interface::writeHeader() +{ + if (m_queryTimeout) + write("(set-option :timeout " + to_string(*m_queryTimeout) + ")"); + write("(set-logic HORN)\n"); +} + +string CHCSmtLib2Interface::forall() +{ + string vars("("); + for (auto const& [name, sort]: m_smtlib2->variables()) + { + solAssert(sort, ""); + if (sort->kind != Kind::Function) + vars += " (" + name + " " + toSmtLibSort(*sort) + ")"; + } + vars += ")"; + return vars; +} + void CHCSmtLib2Interface::declareFunction(string const& _name, SortPointer const& _sort) { smtAssert(_sort, ""); @@ -137,8 +179,8 @@ void CHCSmtLib2Interface::declareFunction(string const& _name, SortPointer const { auto fSort = dynamic_pointer_cast(_sort); smtAssert(fSort->codomain, ""); - string domain = m_smtlib2->toSmtLibSort(fSort->domain); - string codomain = m_smtlib2->toSmtLibSort(*fSort->codomain); + string domain = toSmtLibSort(fSort->domain); + string codomain = toSmtLibSort(*fSort->codomain); m_variables.insert(_name); write( "(declare-fun |" + diff --git a/libsmtutil/CHCSmtLib2Interface.h b/libsmtutil/CHCSmtLib2Interface.h index 17227867e..460895e3b 100644 --- a/libsmtutil/CHCSmtLib2Interface.h +++ b/libsmtutil/CHCSmtLib2Interface.h @@ -53,6 +53,12 @@ public: SMTLib2Interface* smtlib2Interface() const { return m_smtlib2.get(); } private: + std::string toSmtLibSort(Sort const& _sort); + std::string toSmtLibSort(std::vector const& _sort); + + void writeHeader(); + std::string forall(); + void declareFunction(std::string const& _name, SortPointer const& _sort); void write(std::string _data); @@ -70,6 +76,8 @@ private: std::vector m_unhandledQueries; frontend::ReadCallback::Callback m_smtCallback; + + std::map m_sortNames; }; } diff --git a/libsmtutil/CVC4Interface.cpp b/libsmtutil/CVC4Interface.cpp index daac2ab71..8387b24cb 100644 --- a/libsmtutil/CVC4Interface.cpp +++ b/libsmtutil/CVC4Interface.cpp @@ -170,7 +170,7 @@ CVC4::Expr CVC4Interface::toCVC4Expr(Expression const& _expr) return arguments[0].andExpr(arguments[1]); else if (n == "or") return arguments[0].orExpr(arguments[1]); - else if (n == "implies") + else if (n == "=>") return m_context.mkExpr(CVC4::kind::IMPLIES, arguments[0], arguments[1]); else if (n == "=") return m_context.mkExpr(CVC4::kind::EQUAL, arguments[0], arguments[1]); @@ -188,7 +188,7 @@ CVC4::Expr CVC4Interface::toCVC4Expr(Expression const& _expr) return m_context.mkExpr(CVC4::kind::MINUS, arguments[0], arguments[1]); else if (n == "*") return m_context.mkExpr(CVC4::kind::MULT, arguments[0], arguments[1]); - else if (n == "/") + else if (n == "div") return m_context.mkExpr(CVC4::kind::INTS_DIVISION_TOTAL, arguments[0], arguments[1]); else if (n == "mod") return m_context.mkExpr(CVC4::kind::INTS_MODULUS, arguments[0], arguments[1]); diff --git a/libsmtutil/SMTLib2Interface.cpp b/libsmtutil/SMTLib2Interface.cpp index e4ea16545..bd74cf2c9 100644 --- a/libsmtutil/SMTLib2Interface.cpp +++ b/libsmtutil/SMTLib2Interface.cpp @@ -23,6 +23,8 @@ #include #include +#include + #include #include #include @@ -173,7 +175,7 @@ string SMTLib2Interface::toSExpr(Expression const& _expr) sexpr += string("ite ") + "(= ((_ extract " + pos + " " + pos + ")" + arg + ") #b0) " + nat + " " + - "(- (bvneg " + arg + "))"; + "(- (bv2nat (bvneg " + arg + ")))"; } else if (_expr.name == "const_array") { @@ -231,14 +233,15 @@ string SMTLib2Interface::toSmtLibSort(Sort const& _sort) { auto const& tupleSort = dynamic_cast(_sort); string tupleName = "|" + tupleSort.name + "|"; - if (!m_userSorts.count(tupleName)) + auto isName = [&](auto entry) { return entry.first == tupleName; }; + if (ranges::find_if(m_userSorts, isName) == m_userSorts.end()) { - m_userSorts.insert(tupleName); string decl("(declare-datatypes ((" + tupleName + " 0)) (((" + tupleName); smtAssert(tupleSort.members.size() == tupleSort.components.size(), ""); for (unsigned i = 0; i < tupleSort.members.size(); ++i) decl += " (|" + tupleSort.members.at(i) + "| " + toSmtLibSort(*tupleSort.components.at(i)) + ")"; decl += "))))"; + m_userSorts.emplace_back(tupleName, decl); write(decl); } diff --git a/libsmtutil/SMTLib2Interface.h b/libsmtutil/SMTLib2Interface.h index bbe1feb9f..9454d551f 100644 --- a/libsmtutil/SMTLib2Interface.h +++ b/libsmtutil/SMTLib2Interface.h @@ -66,6 +66,8 @@ public: std::map variables() { return m_variables; } + std::vector> const& userSorts() const { return m_userSorts; } + private: void declareFunction(std::string const& _name, SortPointer const& _sort); @@ -79,7 +81,12 @@ private: std::vector m_accumulatedOutput; std::map m_variables; - std::set m_userSorts; + + /// Each pair in this vector represents an SMTChecker created + /// sort (a user sort), and the smtlib2 declaration of that sort. + /// It needs to be a vector so that the declaration order is kept, + /// otherwise solvers cannot parse the queries. + std::vector> m_userSorts; std::map m_queryResponses; std::vector m_unhandledQueries; diff --git a/libsmtutil/SolverInterface.h b/libsmtutil/SolverInterface.h index fb2fa643d..6eb6982fb 100644 --- a/libsmtutil/SolverInterface.h +++ b/libsmtutil/SolverInterface.h @@ -64,8 +64,20 @@ public: name(std::move(_name)), arguments(std::move(_arguments)), sort(std::move(_sort)) {} Expression(size_t _number): Expression(std::to_string(_number), {}, SortProvider::sintSort) {} Expression(u256 const& _number): Expression(_number.str(), {}, SortProvider::sintSort) {} - Expression(s256 const& _number): Expression(_number.str(), {}, SortProvider::sintSort) {} - Expression(bigint const& _number): Expression(_number.str(), {}, SortProvider::sintSort) {} + Expression(s256 const& _number): Expression( + _number >= 0 ? _number.str() : "-", + _number >= 0 ? + std::vector{} : + std::vector{Expression(size_t(0)), bigint(-_number)}, + SortProvider::sintSort + ) {} + Expression(bigint const& _number): Expression( + _number >= 0 ? _number.str() : "-", + _number >= 0 ? + std::vector{} : + std::vector{Expression(size_t(0)), bigint(-_number)}, + SortProvider::sintSort + ) {} Expression(Expression const&) = default; Expression(Expression&&) = default; @@ -86,7 +98,7 @@ public: {"not", 1}, {"and", 2}, {"or", 2}, - {"implies", 2}, + {"=>", 2}, {"=", 2}, {"<", 2}, {"<=", 2}, @@ -95,7 +107,7 @@ public: {"+", 2}, {"-", 2}, {"*", 2}, - {"/", 2}, + {"div", 2}, {"mod", 2}, {"bvnot", 1}, {"bvand", 2}, @@ -126,7 +138,7 @@ public: static Expression implies(Expression _a, Expression _b) { return Expression( - "implies", + "=>", std::move(_a), std::move(_b), Kind::Bool @@ -300,7 +312,7 @@ public: friend Expression operator/(Expression _a, Expression _b) { auto intSort = _a.sort; - return Expression("/", {std::move(_a), std::move(_b)}, intSort); + return Expression("div", {std::move(_a), std::move(_b)}, intSort); } friend Expression operator%(Expression _a, Expression _b) { diff --git a/libsmtutil/Z3Interface.cpp b/libsmtutil/Z3Interface.cpp index 8d7a03392..03105a172 100644 --- a/libsmtutil/Z3Interface.cpp +++ b/libsmtutil/Z3Interface.cpp @@ -189,7 +189,7 @@ z3::expr Z3Interface::toZ3Expr(Expression const& _expr) return arguments[0] && arguments[1]; else if (n == "or") return arguments[0] || arguments[1]; - else if (n == "implies") + else if (n == "=>") return z3::implies(arguments[0], arguments[1]); else if (n == "=") return arguments[0] == arguments[1]; @@ -207,7 +207,7 @@ z3::expr Z3Interface::toZ3Expr(Expression const& _expr) return arguments[0] - arguments[1]; else if (n == "*") return arguments[0] * arguments[1]; - else if (n == "/") + else if (n == "div") return arguments[0] / arguments[1]; else if (n == "mod") return z3::mod(arguments[0], arguments[1]); diff --git a/libsolc/libsolc.cpp b/libsolc/libsolc.cpp index f87f075d6..48cf7e0f8 100644 --- a/libsolc/libsolc.cpp +++ b/libsolc/libsolc.cpp @@ -25,8 +25,6 @@ #include #include #include -#include -#include #include #include diff --git a/libsolidity/CMakeLists.txt b/libsolidity/CMakeLists.txt index 6b8426a1e..24b7d2d85 100644 --- a/libsolidity/CMakeLists.txt +++ b/libsolidity/CMakeLists.txt @@ -10,6 +10,8 @@ set(sources analysis/ControlFlowBuilder.h analysis/ControlFlowGraph.cpp analysis/ControlFlowGraph.h + analysis/ControlFlowRevertPruner.cpp + analysis/ControlFlowRevertPruner.h analysis/DeclarationContainer.cpp analysis/DeclarationContainer.h analysis/DeclarationTypeChecker.cpp diff --git a/libsolidity/analysis/ContractLevelChecker.cpp b/libsolidity/analysis/ContractLevelChecker.cpp index 8158b410d..631545569 100644 --- a/libsolidity/analysis/ContractLevelChecker.cpp +++ b/libsolidity/analysis/ContractLevelChecker.cpp @@ -86,6 +86,7 @@ bool ContractLevelChecker::check(ContractDefinition const& _contract) checkDuplicateFunctions(_contract); checkDuplicateEvents(_contract); + checkReceiveFunction(_contract); m_overrideChecker.check(_contract); checkBaseConstructorArguments(_contract); checkAbstractDefinitions(_contract); @@ -162,6 +163,35 @@ void ContractLevelChecker::checkDuplicateEvents(ContractDefinition const& _contr findDuplicateDefinitions(events); } +void ContractLevelChecker::checkReceiveFunction(ContractDefinition const& _contract) +{ + for (FunctionDefinition const* function: _contract.definedFunctions()) + { + solAssert(function, ""); + if (function->isReceive()) + { + if (function->libraryFunction()) + m_errorReporter.declarationError(4549_error, function->location(), "Libraries cannot have receive ether functions."); + + if (function->stateMutability() != StateMutability::Payable) + m_errorReporter.declarationError( + 7793_error, + function->location(), + "Receive ether function must be payable, but is \"" + + stateMutabilityToString(function->stateMutability()) + + "\"." + ); + if (function->visibility() != Visibility::External) + m_errorReporter.declarationError(4095_error, function->location(), "Receive ether function must be defined as \"external\"."); + + if (!function->returnParameters().empty()) + m_errorReporter.fatalDeclarationError(6899_error, function->returnParameterList()->location(), "Receive ether function cannot return values."); + if (!function->parameters().empty()) + m_errorReporter.fatalDeclarationError(6857_error, function->parameterList().location(), "Receive ether function cannot take parameters."); + } + } +} + template void ContractLevelChecker::findDuplicateDefinitions(map> const& _definitions) { diff --git a/libsolidity/analysis/ContractLevelChecker.h b/libsolidity/analysis/ContractLevelChecker.h index 5f890681a..1f5826fdc 100644 --- a/libsolidity/analysis/ContractLevelChecker.h +++ b/libsolidity/analysis/ContractLevelChecker.h @@ -63,6 +63,7 @@ private: /// arguments and that there is at most one constructor. void checkDuplicateFunctions(ContractDefinition const& _contract); void checkDuplicateEvents(ContractDefinition const& _contract); + void checkReceiveFunction(ContractDefinition const& _contract); template void findDuplicateDefinitions(std::map> const& _definitions); /// Checks for unimplemented functions and modifiers. diff --git a/libsolidity/analysis/ControlFlowAnalyzer.cpp b/libsolidity/analysis/ControlFlowAnalyzer.cpp index 1d8bf92ba..8ea82bc43 100644 --- a/libsolidity/analysis/ControlFlowAnalyzer.cpp +++ b/libsolidity/analysis/ControlFlowAnalyzer.cpp @@ -22,28 +22,45 @@ #include #include +#include + using namespace std; +using namespace std::placeholders; using namespace solidity::langutil; using namespace solidity::frontend; -bool ControlFlowAnalyzer::analyze(ASTNode const& _astRoot) + +bool ControlFlowAnalyzer::run() { - _astRoot.accept(*this); + for (auto& [pair, flow]: m_cfg.allFunctionFlows()) + analyze(*pair.function, pair.contract, *flow); + return Error::containsOnlyWarnings(m_errorReporter.errors()); } -bool ControlFlowAnalyzer::visit(FunctionDefinition const& _function) +void ControlFlowAnalyzer::analyze(FunctionDefinition const& _function, ContractDefinition const* _contract, FunctionFlow const& _flow) { - if (_function.isImplemented()) - { - auto const& functionFlow = m_cfg.functionFlow(_function); - checkUninitializedAccess(functionFlow.entry, functionFlow.exit, _function.body().statements().empty()); - checkUnreachable(functionFlow.entry, functionFlow.exit, functionFlow.revert, functionFlow.transactionReturn); - } - return false; + if (!_function.isImplemented()) + return; + + optional mostDerivedContractName; + + // The name of the most derived contract only required if it differs from + // the functions contract + if (_contract && _contract != _function.annotation().contract) + mostDerivedContractName = _contract->name(); + + checkUninitializedAccess( + _flow.entry, + _flow.exit, + _function.body().statements().empty(), + mostDerivedContractName + ); + checkUnreachable(_flow.entry, _flow.exit, _flow.revert, _flow.transactionReturn); } -void ControlFlowAnalyzer::checkUninitializedAccess(CFGNode const* _entry, CFGNode const* _exit, bool _emptyBody) const + +void ControlFlowAnalyzer::checkUninitializedAccess(CFGNode const* _entry, CFGNode const* _exit, bool _emptyBody, optional _contractName) { struct NodeInfo { @@ -156,16 +173,27 @@ void ControlFlowAnalyzer::checkUninitializedAccess(CFGNode const* _entry, CFGNod " without prior assignment, which would lead to undefined behaviour." ); else if (!_emptyBody && varDecl.name().empty()) + { + if (!m_unassignedReturnVarsAlreadyWarnedFor.emplace(&varDecl).second) + continue; + m_errorReporter.warning( 6321_error, varDecl.location(), - "Unnamed return variable can remain unassigned. Add an explicit return with value to all non-reverting code paths or name the variable." + "Unnamed return variable can remain unassigned" + + ( + _contractName.has_value() ? + " when the function is called when \"" + _contractName.value() + "\" is the most derived contract." : + "." + ) + + " Add an explicit return with value to all non-reverting code paths or name the variable." ); + } } } } -void ControlFlowAnalyzer::checkUnreachable(CFGNode const* _entry, CFGNode const* _exit, CFGNode const* _revert, CFGNode const* _transactionReturn) const +void ControlFlowAnalyzer::checkUnreachable(CFGNode const* _entry, CFGNode const* _exit, CFGNode const* _revert, CFGNode const* _transactionReturn) { // collect all nodes reachable from the entry point std::set reachable = util::BreadthFirstSearch{{_entry}}.run( @@ -193,6 +221,8 @@ void ControlFlowAnalyzer::checkUnreachable(CFGNode const* _entry, CFGNode const* // Extend the location, as long as the next location overlaps (unreachable is sorted). for (; it != unreachable.end() && it->start <= location.end; ++it) location.end = std::max(location.end, it->end); - m_errorReporter.warning(5740_error, location, "Unreachable code."); + + if (m_unreachableLocationsAlreadyWarnedFor.emplace(location).second) + m_errorReporter.warning(5740_error, location, "Unreachable code."); } } diff --git a/libsolidity/analysis/ControlFlowAnalyzer.h b/libsolidity/analysis/ControlFlowAnalyzer.h index 4e53fef78..b0d609d72 100644 --- a/libsolidity/analysis/ControlFlowAnalyzer.h +++ b/libsolidity/analysis/ControlFlowAnalyzer.h @@ -19,30 +19,38 @@ #pragma once #include +#include #include namespace solidity::frontend { -class ControlFlowAnalyzer: private ASTConstVisitor +class ControlFlowAnalyzer { public: explicit ControlFlowAnalyzer(CFG const& _cfg, langutil::ErrorReporter& _errorReporter): m_cfg(_cfg), m_errorReporter(_errorReporter) {} - bool analyze(ASTNode const& _astRoot); - - bool visit(FunctionDefinition const& _function) override; + bool run(); private: + void analyze(FunctionDefinition const& _function, ContractDefinition const* _contract, FunctionFlow const& _flow); /// Checks for uninitialized variable accesses in the control flow between @param _entry and @param _exit. - void checkUninitializedAccess(CFGNode const* _entry, CFGNode const* _exit, bool _emptyBody) const; + /// @param _entry entry node + /// @param _exit exit node + /// @param _emptyBody whether the body of the function is empty (true) or not (false) + /// @param _contractName name of the most derived contract, should be empty + /// if the function is also defined in it + void checkUninitializedAccess(CFGNode const* _entry, CFGNode const* _exit, bool _emptyBody, std::optional _contractName = {}); /// Checks for unreachable code, i.e. code ending in @param _exit, @param _revert or @param _transactionReturn /// that can not be reached from @param _entry. - void checkUnreachable(CFGNode const* _entry, CFGNode const* _exit, CFGNode const* _revert, CFGNode const* _transactionReturn) const; + void checkUnreachable(CFGNode const* _entry, CFGNode const* _exit, CFGNode const* _revert, CFGNode const* _transactionReturn); CFG const& m_cfg; langutil::ErrorReporter& m_errorReporter; + + std::set m_unreachableLocationsAlreadyWarnedFor; + std::set m_unassignedReturnVarsAlreadyWarnedFor; }; } diff --git a/libsolidity/analysis/ControlFlowBuilder.cpp b/libsolidity/analysis/ControlFlowBuilder.cpp index 8946e6ed3..98dcbf069 100644 --- a/libsolidity/analysis/ControlFlowBuilder.cpp +++ b/libsolidity/analysis/ControlFlowBuilder.cpp @@ -260,6 +260,7 @@ bool ControlFlowBuilder::visit(PlaceholderStatement const&) bool ControlFlowBuilder::visit(FunctionCall const& _functionCall) { + solAssert(!!m_revertNode, ""); solAssert(!!m_currentNode, ""); solAssert(!!_functionCall.expression().annotation().type, ""); @@ -267,26 +268,44 @@ bool ControlFlowBuilder::visit(FunctionCall const& _functionCall) switch (functionType->kind()) { case FunctionType::Kind::Revert: - solAssert(!!m_revertNode, ""); visitNode(_functionCall); _functionCall.expression().accept(*this); ASTNode::listAccept(_functionCall.arguments(), *this); + connect(m_currentNode, m_revertNode); + m_currentNode = newLabel(); return false; case FunctionType::Kind::Require: case FunctionType::Kind::Assert: { - solAssert(!!m_revertNode, ""); visitNode(_functionCall); _functionCall.expression().accept(*this); ASTNode::listAccept(_functionCall.arguments(), *this); + connect(m_currentNode, m_revertNode); + auto nextNode = newLabel(); + connect(m_currentNode, nextNode); m_currentNode = nextNode; return false; } + case FunctionType::Kind::Internal: + { + visitNode(_functionCall); + _functionCall.expression().accept(*this); + ASTNode::listAccept(_functionCall.arguments(), *this); + + m_currentNode->functionCalls.emplace_back(&_functionCall); + + auto nextNode = newLabel(); + + connect(m_currentNode, nextNode); + m_currentNode = nextNode; + + return false; + } default: break; } @@ -303,7 +322,7 @@ bool ControlFlowBuilder::visit(ModifierInvocation const& _modifierInvocation) _modifierInvocation.name().annotation().referencedDeclaration ); if (!modifierDefinition) return false; - solAssert(!!modifierDefinition, ""); + if (!modifierDefinition->isImplemented()) return false; solAssert(!!m_returnNode, ""); m_placeholderEntry = newLabel(); @@ -482,7 +501,7 @@ void ControlFlowBuilder::operator()(yul::Identifier const& _identifier) m_currentNode->variableOccurrences.emplace_back( *declaration, VariableOccurrence::Kind::Access, - _identifier.location + _identifier.debugData->location ); } } @@ -498,7 +517,7 @@ void ControlFlowBuilder::operator()(yul::Assignment const& _assignment) m_currentNode->variableOccurrences.emplace_back( *declaration, VariableOccurrence::Kind::Assignment, - variable.location + variable.debugData->location ); } diff --git a/libsolidity/analysis/ControlFlowBuilder.h b/libsolidity/analysis/ControlFlowBuilder.h index 879d9b241..9cab99dd4 100644 --- a/libsolidity/analysis/ControlFlowBuilder.h +++ b/libsolidity/analysis/ControlFlowBuilder.h @@ -26,11 +26,11 @@ #include #include -namespace solidity::frontend { +namespace solidity::frontend +{ -/** Helper class that builds the control flow of a function or modifier. - * Modifiers are not yet applied to the functions. This is done in a second - * step in the CFG class. +/** + * Helper class that builds the control flow of a function or modifier. */ class ControlFlowBuilder: private ASTConstVisitor, private yul::ASTWalker { diff --git a/libsolidity/analysis/ControlFlowGraph.cpp b/libsolidity/analysis/ControlFlowGraph.cpp index f7344e7fc..66ec552ca 100644 --- a/libsolidity/analysis/ControlFlowGraph.cpp +++ b/libsolidity/analysis/ControlFlowGraph.cpp @@ -19,7 +19,6 @@ #include #include -#include using namespace std; using namespace solidity::langutil; @@ -34,15 +33,25 @@ bool CFG::constructFlow(ASTNode const& _astRoot) bool CFG::visit(FunctionDefinition const& _function) { - if (_function.isImplemented()) - m_functionControlFlow[&_function] = ControlFlowBuilder::createFunctionFlow(m_nodeContainer, _function); + if (_function.isImplemented() && _function.isFree()) + m_functionControlFlow[{nullptr, &_function}] = ControlFlowBuilder::createFunctionFlow(m_nodeContainer, _function); return false; } -FunctionFlow const& CFG::functionFlow(FunctionDefinition const& _function) const +bool CFG::visit(ContractDefinition const& _contract) { - solAssert(m_functionControlFlow.count(&_function), ""); - return *m_functionControlFlow.find(&_function)->second; + for (ContractDefinition const* contract: _contract.annotation().linearizedBaseContracts) + for (FunctionDefinition const* function: contract->definedFunctions()) + if (function->isImplemented()) + m_functionControlFlow[{&_contract, function}] = + ControlFlowBuilder::createFunctionFlow(m_nodeContainer, *function); + + return true; +} + +FunctionFlow const& CFG::functionFlow(FunctionDefinition const& _function, ContractDefinition const* _contract) const +{ + return *m_functionControlFlow.at({_contract, &_function}); } CFGNode* CFG::NodeContainer::newNode() diff --git a/libsolidity/analysis/ControlFlowGraph.h b/libsolidity/analysis/ControlFlowGraph.h index e3c17f2d1..732548664 100644 --- a/libsolidity/analysis/ControlFlowGraph.h +++ b/libsolidity/analysis/ControlFlowGraph.h @@ -98,6 +98,8 @@ struct CFGNode std::vector entries; /// Exit nodes. All CFG nodes to which control flow may continue after this node. std::vector exits; + /// Function calls done by this node + std::vector functionCalls; /// Variable occurrences in the node. std::vector variableOccurrences; @@ -118,7 +120,7 @@ struct FunctionFlow /// (e.g. all return statements of the function). CFGNode* exit = nullptr; /// Revert node. Control flow of the function in case of revert. - /// This node is empty does not have any exits, but may have multiple entries + /// This node is empty and does not have any exits, but may have multiple entries /// (e.g. all assert, require, revert and throw statements). CFGNode* revert = nullptr; /// Transaction return node. Destination node for inline assembly "return" calls. @@ -130,13 +132,37 @@ struct FunctionFlow class CFG: private ASTConstVisitor { public: + struct FunctionContractTuple + { + ContractDefinition const* contract = nullptr; + FunctionDefinition const* function = nullptr; + + // Use AST ids for comparison to keep a deterministic order in the + // containers using this struct + bool operator<(FunctionContractTuple const& _other) const + { + return + std::make_pair(contract ? contract->id() : -1, function->id()) < + std::make_pair(_other.contract ? _other.contract->id() : -1, _other.function->id()); + } + }; explicit CFG(langutil::ErrorReporter& _errorReporter): m_errorReporter(_errorReporter) {} bool constructFlow(ASTNode const& _astRoot); bool visit(FunctionDefinition const& _function) override; + bool visit(ContractDefinition const& _contract) override; - FunctionFlow const& functionFlow(FunctionDefinition const& _function) const; + /// Get the function flow for the given function, using `_contract` as the + /// most derived contract + /// @param _function function to find the function flow for + /// @param _contract most derived contract or nullptr for free functions + FunctionFlow const& functionFlow(FunctionDefinition const& _function, ContractDefinition const* _contract = nullptr) const; + + std::map> const& allFunctionFlows() const + { + return m_functionControlFlow; + } class NodeContainer { @@ -153,7 +179,7 @@ private: /// are owned by the CFG class and stored in this container. NodeContainer m_nodeContainer; - std::map> m_functionControlFlow; + std::map> m_functionControlFlow; }; } diff --git a/libsolidity/analysis/ControlFlowRevertPruner.cpp b/libsolidity/analysis/ControlFlowRevertPruner.cpp new file mode 100644 index 000000000..6a8018355 --- /dev/null +++ b/libsolidity/analysis/ControlFlowRevertPruner.cpp @@ -0,0 +1,171 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 + +#include + +#include + +#include + + +namespace solidity::frontend +{ + +namespace +{ + +/// Find the right scope for the called function: When calling a base function, +/// we keep the most derived, but we use the called contract in case it is a +/// library function or nullptr for a free function. +ContractDefinition const* findScopeContract(FunctionDefinition const& _function, ContractDefinition const* _callingContract) +{ + if (auto const* functionContract = _function.annotation().contract) + { + if (_callingContract && _callingContract->derivesFrom(*functionContract)) + return _callingContract; + else + return functionContract; + } + + return nullptr; +} +} + +void ControlFlowRevertPruner::run() +{ + for (auto& [pair, flow]: m_cfg.allFunctionFlows()) + m_functions[pair] = RevertState::Unknown; + + findRevertStates(); + modifyFunctionFlows(); +} + +void ControlFlowRevertPruner::findRevertStates() +{ + std::set pendingFunctions = keys(m_functions); + // We interrupt the search whenever we encounter a call to a function with (yet) unknown + // revert behaviour. The ``wakeUp`` data structure contains information about which + // searches to restart once we know about the behaviour. + std::map> wakeUp; + + while (!pendingFunctions.empty()) + { + CFG::FunctionContractTuple item = *pendingFunctions.begin(); + pendingFunctions.erase(pendingFunctions.begin()); + + if (m_functions[item] != RevertState::Unknown) + continue; + + bool foundExit = false; + bool foundUnknown = false; + + FunctionFlow const& functionFlow = m_cfg.functionFlow(*item.function, item.contract); + + solidity::util::BreadthFirstSearch{{functionFlow.entry}}.run( + [&](CFGNode* _node, auto&& _addChild) { + if (_node == functionFlow.exit) + foundExit = true; + + for (auto const* functionCall: _node->functionCalls) + { + auto const* resolvedFunction = ASTNode::resolveFunctionCall(*functionCall, item.contract); + + if (resolvedFunction == nullptr || !resolvedFunction->isImplemented()) + continue; + + CFG::FunctionContractTuple calledFunctionTuple{ + findScopeContract(*resolvedFunction, item.contract), + resolvedFunction + }; + switch (m_functions.at(calledFunctionTuple)) + { + case RevertState::Unknown: + wakeUp[calledFunctionTuple].insert(item); + foundUnknown = true; + return; + case RevertState::AllPathsRevert: + return; + case RevertState::HasNonRevertingPath: + break; + } + } + + for (CFGNode* exit: _node->exits) + _addChild(exit); + } + ); + + auto& revertState = m_functions[item]; + + if (foundExit) + revertState = RevertState::HasNonRevertingPath; + else if (!foundUnknown) + revertState = RevertState::AllPathsRevert; + + if (revertState != RevertState::Unknown && wakeUp.count(item)) + { + // Restart all searches blocked by this function. + for (CFG::FunctionContractTuple const& nextItem: wakeUp[item]) + if (m_functions.at(nextItem) == RevertState::Unknown) + pendingFunctions.insert(nextItem); + wakeUp.erase(item); + } + } +} + +void ControlFlowRevertPruner::modifyFunctionFlows() +{ + for (auto& item: m_functions) + { + FunctionFlow const& functionFlow = m_cfg.functionFlow(*item.first.function, item.first.contract); + solidity::util::BreadthFirstSearch{{functionFlow.entry}}.run( + [&](CFGNode* _node, auto&& _addChild) { + for (auto const* functionCall: _node->functionCalls) + { + auto const* resolvedFunction = ASTNode::resolveFunctionCall(*functionCall, item.first.contract); + + if (resolvedFunction == nullptr || !resolvedFunction->isImplemented()) + continue; + + switch (m_functions.at({findScopeContract(*resolvedFunction, item.first.contract), resolvedFunction})) + { + case RevertState::Unknown: + [[fallthrough]]; + case RevertState::AllPathsRevert: + // If the revert states of the functions do not + // change anymore, we treat all "unknown" states as + // "reverting", since they can only be caused by + // recursion. + for (CFGNode * node: _node->exits) + ranges::remove(node->entries, _node); + + _node->exits = {functionFlow.revert}; + functionFlow.revert->entries.push_back(_node); + return; + default: + break; + } + } + + for (CFGNode* exit: _node->exits) + _addChild(exit); + }); + } +} + +} diff --git a/libsolidity/analysis/ControlFlowRevertPruner.h b/libsolidity/analysis/ControlFlowRevertPruner.h new file mode 100644 index 000000000..9702ddfb7 --- /dev/null +++ b/libsolidity/analysis/ControlFlowRevertPruner.h @@ -0,0 +1,66 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 + +#pragma once + +#include +#include + +#include + +namespace solidity::frontend +{ + +/** + * Analyses all function flows and recursively removes all exit edges from CFG + * nodes that make function calls that will always revert. + */ +class ControlFlowRevertPruner +{ +public: + ControlFlowRevertPruner(CFG& _cfg): m_cfg(_cfg) {} + + void run(); +private: + /// Possible revert states of a function call + enum class RevertState + { + AllPathsRevert, + HasNonRevertingPath, + Unknown, + }; + + /// Identify revert states of all function flows + void findRevertStates(); + + /// Modify function flows so that edges with reverting function calls are removed + void modifyFunctionFlows(); + + /// Control Flow Graph object. + CFG& m_cfg; + + /// function/contract pairs mapped to their according revert state + std::map m_functions; + + std::map< + std::tuple, + FunctionDefinition const* + > m_resolveCache; + +}; +} diff --git a/libsolidity/analysis/DeclarationContainer.cpp b/libsolidity/analysis/DeclarationContainer.cpp index 499c26ff1..7ca134c5d 100644 --- a/libsolidity/analysis/DeclarationContainer.cpp +++ b/libsolidity/analysis/DeclarationContainer.cpp @@ -24,9 +24,11 @@ #include #include -#include #include +#include +#include + using namespace std; using namespace solidity; using namespace solidity::frontend; @@ -121,11 +123,7 @@ bool DeclarationContainer::registerDeclaration( if (conflictingDeclaration(_declaration, _name)) return false; - // Do not warn about shadowing for structs and enums because their members are - // not accessible without prefixes. Also do not warn about event parameters - // because they do not participate in any proper scope. - bool special = _declaration.scope() && (_declaration.isStructMember() || _declaration.isEnumValue() || _declaration.isEventOrErrorParameter()); - if (m_enclosingContainer && !special) + if (m_enclosingContainer && _declaration.isVisibleAsUnqualifiedName()) m_homonymCandidates.emplace_back(*_name, _location ? _location : &_declaration.location()); } @@ -144,16 +142,35 @@ bool DeclarationContainer::registerDeclaration( return registerDeclaration(_declaration, nullptr, nullptr, _invisible, _update); } -vector DeclarationContainer::resolveName(ASTString const& _name, bool _recursive, bool _alsoInvisible) const +vector DeclarationContainer::resolveName( + ASTString const& _name, + bool _recursive, + bool _alsoInvisible, + bool _onlyVisibleAsUnqualifiedNames +) const { solAssert(!_name.empty(), "Attempt to resolve empty name."); vector result; + if (m_declarations.count(_name)) - result = m_declarations.at(_name); + { + if (_onlyVisibleAsUnqualifiedNames) + result += m_declarations.at(_name) | ranges::views::filter(&Declaration::isVisibleAsUnqualifiedName) | ranges::to_vector; + else + result += m_declarations.at(_name); + } + if (_alsoInvisible && m_invisibleDeclarations.count(_name)) - result += m_invisibleDeclarations.at(_name); + { + if (_onlyVisibleAsUnqualifiedNames) + result += m_invisibleDeclarations.at(_name) | ranges::views::filter(&Declaration::isVisibleAsUnqualifiedName) | ranges::to_vector; + else + result += m_invisibleDeclarations.at(_name); + } + if (result.empty() && _recursive && m_enclosingContainer) - result = m_enclosingContainer->resolveName(_name, true, _alsoInvisible); + result = m_enclosingContainer->resolveName(_name, true, _alsoInvisible, _onlyVisibleAsUnqualifiedNames); + return result; } diff --git a/libsolidity/analysis/DeclarationContainer.h b/libsolidity/analysis/DeclarationContainer.h index 2f29425a3..2dbed0098 100644 --- a/libsolidity/analysis/DeclarationContainer.h +++ b/libsolidity/analysis/DeclarationContainer.h @@ -39,11 +39,10 @@ class DeclarationContainer public: using Homonyms = std::vector>>; - explicit DeclarationContainer( - ASTNode const* _enclosingNode = nullptr, - DeclarationContainer* _enclosingContainer = nullptr - ): - m_enclosingNode(_enclosingNode), m_enclosingContainer(_enclosingContainer) + DeclarationContainer() = default; + explicit DeclarationContainer(ASTNode const* _enclosingNode, DeclarationContainer* _enclosingContainer): + m_enclosingNode(_enclosingNode), + m_enclosingContainer(_enclosingContainer) { if (_enclosingContainer) _enclosingContainer->m_innerContainers.emplace_back(this); @@ -57,7 +56,20 @@ public: bool registerDeclaration(Declaration const& _declaration, ASTString const* _name, langutil::SourceLocation const* _location, bool _invisible, bool _update); bool registerDeclaration(Declaration const& _declaration, bool _invisible, bool _update); - std::vector resolveName(ASTString const& _name, bool _recursive = false, bool _alsoInvisible = false) const; + /// Finds all declarations that in the current scope can be referred to using specified name. + /// @param _name the name to look for. + /// @param _recursive if true and there are no matching declarations in the current container, + /// recursively searches the enclosing containers as well. + /// @param _alsoInvisible if true, include invisible declaration in the results. + /// @param _onlyVisibleAsUnqualifiedNames if true, do not include declarations which can never + /// actually be referenced using their name alone (without being qualified with the name + /// of scope in which they are declared). + std::vector resolveName( + ASTString const& _name, + bool _recursive = false, + bool _alsoInvisible = false, + bool _onlyVisibleAsUnqualifiedNames = false + ) const; ASTNode const* enclosingNode() const { return m_enclosingNode; } DeclarationContainer const* enclosingContainer() const { return m_enclosingContainer; } std::map> const& declarations() const { return m_declarations; } @@ -80,8 +92,8 @@ public: void populateHomonyms(std::back_insert_iterator _it) const; private: - ASTNode const* m_enclosingNode; - DeclarationContainer const* m_enclosingContainer; + ASTNode const* m_enclosingNode = nullptr; + DeclarationContainer const* m_enclosingContainer = nullptr; std::vector m_innerContainers; std::map> m_declarations; std::map> m_invisibleDeclarations; diff --git a/libsolidity/analysis/DocStringTagParser.cpp b/libsolidity/analysis/DocStringTagParser.cpp index 75a8dcc6d..52fb4aebe 100644 --- a/libsolidity/analysis/DocStringTagParser.cpp +++ b/libsolidity/analysis/DocStringTagParser.cpp @@ -47,6 +47,68 @@ bool DocStringTagParser::parseDocStrings(SourceUnit const& _sourceUnit) return errorWatcher.ok(); } +bool DocStringTagParser::validateDocStringsUsingTypes(SourceUnit const& _sourceUnit) +{ + ErrorReporter::ErrorWatcher errorWatcher = m_errorReporter.errorWatcher(); + + SimpleASTVisitor visitReturns( + [](ASTNode const&) { return true; }, + [&](ASTNode const& _node) + { + if (auto const* annotation = dynamic_cast(&_node.annotation())) + { + auto const& documentationNode = dynamic_cast(_node); + + size_t returnTagsVisited = 0; + + for (auto const& [tagName, tagValue]: annotation->docTags) + if (tagName == "return") + { + returnTagsVisited++; + vector returnParameterNames; + + if (auto const* varDecl = dynamic_cast(&_node)) + { + if (!varDecl->isPublic()) + continue; + + // FunctionType() requires the DeclarationTypeChecker to have run. + returnParameterNames = FunctionType(*varDecl).returnParameterNames(); + } + else if (auto const* function = dynamic_cast(&_node)) + returnParameterNames = FunctionType(*function).returnParameterNames(); + else + continue; + + string content = tagValue.content; + string firstWord = content.substr(0, content.find_first_of(" \t")); + + if (returnTagsVisited > returnParameterNames.size()) + m_errorReporter.docstringParsingError( + 2604_error, + documentationNode.documentation()->location(), + "Documentation tag \"@" + tagName + " " + content + "\"" + + " exceeds the number of return parameters." + ); + else + { + string const& parameter = returnParameterNames.at(returnTagsVisited - 1); + if (!parameter.empty() && parameter != firstWord) + m_errorReporter.docstringParsingError( + 5856_error, + documentationNode.documentation()->location(), + "Documentation tag \"@" + tagName + " " + content + "\"" + + " does not contain the name of its return parameter." + ); + } + } + } + }); + + _sourceUnit.accept(visitReturns); + return errorWatcher.ok(); +} + bool DocStringTagParser::visit(ContractDefinition const& _contract) { static set const validTags = set{"author", "title", "dev", "notice"}; @@ -169,7 +231,6 @@ void DocStringTagParser::parseDocStrings( _annotation.docTags = DocStringParser{*_node.documentation(), m_errorReporter}.parse(); - size_t returnTagsVisited = 0; for (auto const& [tagName, tagValue]: _annotation.docTags) { string static const customPrefix("custom:"); @@ -196,43 +257,6 @@ void DocStringTagParser::parseDocStrings( _node.documentation()->location(), "Documentation tag @" + tagName + " not valid for " + _nodeName + "." ); - else if (tagName == "return") - { - returnTagsVisited++; - if (auto const* varDecl = dynamic_cast(&_node)) - { - solAssert(varDecl->isPublic(), "@return is only allowed on public state-variables."); - if (returnTagsVisited > 1) - m_errorReporter.docstringParsingError( - 5256_error, - _node.documentation()->location(), - "Documentation tag \"@" + tagName + "\" is only allowed once on state-variables." - ); - } - else if (auto const* function = dynamic_cast(&_node)) - { - string content = tagValue.content; - string firstWord = content.substr(0, content.find_first_of(" \t")); - - if (returnTagsVisited > function->returnParameters().size()) - m_errorReporter.docstringParsingError( - 2604_error, - _node.documentation()->location(), - "Documentation tag \"@" + tagName + " " + tagValue.content + "\"" + - " exceeds the number of return parameters." - ); - else - { - auto parameter = function->returnParameters().at(returnTagsVisited - 1); - if (!parameter->name().empty() && parameter->name() != firstWord) - m_errorReporter.docstringParsingError( - 5856_error, - _node.documentation()->location(), - "Documentation tag \"@" + tagName + " " + tagValue.content + "\"" + - " does not contain the name of its return parameter." - ); - } - } - } } } + diff --git a/libsolidity/analysis/DocStringTagParser.h b/libsolidity/analysis/DocStringTagParser.h index 548de1d42..84fde6c6c 100644 --- a/libsolidity/analysis/DocStringTagParser.h +++ b/libsolidity/analysis/DocStringTagParser.h @@ -37,6 +37,9 @@ class DocStringTagParser: private ASTConstVisitor public: explicit DocStringTagParser(langutil::ErrorReporter& _errorReporter): m_errorReporter(_errorReporter) {} bool parseDocStrings(SourceUnit const& _sourceUnit); + /// Validate the parsed doc strings, requires parseDocStrings() and the + /// DeclarationTypeChecker to have run. + bool validateDocStringsUsingTypes(SourceUnit const& _sourceUnit); private: bool visit(ContractDefinition const& _contract) override; diff --git a/libsolidity/analysis/FunctionCallGraph.cpp b/libsolidity/analysis/FunctionCallGraph.cpp index 92cd10cd9..265237128 100644 --- a/libsolidity/analysis/FunctionCallGraph.cpp +++ b/libsolidity/analysis/FunctionCallGraph.cpp @@ -25,7 +25,6 @@ #include using namespace std; -using namespace ranges; using namespace solidity::frontend; using namespace solidity::util; @@ -35,7 +34,7 @@ CallGraph FunctionCallGraphBuilder::buildCreationGraph(ContractDefinition const& solAssert(builder.m_currentNode == CallGraph::Node(CallGraph::SpecialNode::Entry), ""); // Create graph for constructor, state vars, etc - for (ContractDefinition const* base: _contract.annotation().linearizedBaseContracts | views::reverse) + for (ContractDefinition const* base: _contract.annotation().linearizedBaseContracts | ranges::views::reverse) { // The constructor and functions called in state variable initial assignments should have // an edge from Entry @@ -76,7 +75,7 @@ CallGraph FunctionCallGraphBuilder::buildDeployedGraph( auto getSecondElement = [](auto const& _tuple){ return get<1>(_tuple); }; // Create graph for all publicly reachable functions - for (FunctionTypePointer functionType: _contract.interfaceFunctionList() | views::transform(getSecondElement)) + for (FunctionTypePointer functionType: _contract.interfaceFunctionList() | ranges::views::transform(getSecondElement)) { auto const* function = dynamic_cast(&functionType->declaration()); auto const* variable = dynamic_cast(&functionType->declaration()); @@ -97,7 +96,8 @@ CallGraph FunctionCallGraphBuilder::buildDeployedGraph( // All functions present in internal dispatch at creation time could potentially be pointers // assigned to state variables and as such may be reachable after deployment as well. builder.m_currentNode = CallGraph::SpecialNode::InternalDispatch; - for (CallGraph::Node const& dispatchTarget: valueOrDefault(_creationGraph.edges, CallGraph::SpecialNode::InternalDispatch, {})) + set defaultNode; + for (CallGraph::Node const& dispatchTarget: valueOrDefault(_creationGraph.edges, CallGraph::SpecialNode::InternalDispatch, defaultNode)) { solAssert(!holds_alternative(dispatchTarget), ""); solAssert(get(dispatchTarget) != nullptr, ""); @@ -305,7 +305,7 @@ ostream& solidity::frontend::operator<<(ostream& _out, CallGraph::Node const& _n auto const* modifier = dynamic_cast(callableDeclaration); auto typeToString = [](auto const& _var) -> string { return _var->type()->toString(true); }; - vector parameters = callableDeclaration->parameters() | views::transform(typeToString) | to>(); + vector parameters = callableDeclaration->parameters() | ranges::views::transform(typeToString) | ranges::to>(); string scopeName; if (!function || !function->isFree()) diff --git a/libsolidity/analysis/NameAndTypeResolver.cpp b/libsolidity/analysis/NameAndTypeResolver.cpp index 5645111f1..97308f7e7 100644 --- a/libsolidity/analysis/NameAndTypeResolver.cpp +++ b/libsolidity/analysis/NameAndTypeResolver.cpp @@ -182,7 +182,13 @@ vector NameAndTypeResolver::nameFromCurrentScope(ASTString c Declaration const* NameAndTypeResolver::pathFromCurrentScope(vector const& _path) const { solAssert(!_path.empty(), ""); - vector candidates = m_currentScope->resolveName(_path.front(), true); + vector candidates = m_currentScope->resolveName( + _path.front(), + /* _recursive */ true, + /* _alsoInvisible */ false, + /* _onlyVisibleAsUnqualifiedNames */ true + ); + for (size_t i = 1; i < _path.size() && candidates.size() == 1; i++) { if (!m_scopes.count(candidates.front())) @@ -627,7 +633,10 @@ void DeclarationRegistrationHelper::enterNewSubScope(ASTNode& _subScope) solAssert(dynamic_cast(&_subScope), "Unexpected scope type."); else { - bool newlyAdded = m_scopes.emplace(&_subScope, make_shared(m_currentScope, m_scopes[m_currentScope].get())).second; + bool newlyAdded = m_scopes.emplace( + &_subScope, + make_shared(m_currentScope, m_scopes[m_currentScope].get()) + ).second; solAssert(newlyAdded, "Unable to add new scope."); } m_currentScope = &_subScope; diff --git a/libsolidity/analysis/OverrideChecker.cpp b/libsolidity/analysis/OverrideChecker.cpp index 0b87f9502..18a40be40 100644 --- a/libsolidity/analysis/OverrideChecker.cpp +++ b/libsolidity/analysis/OverrideChecker.cpp @@ -573,6 +573,19 @@ void OverrideChecker::checkOverride(OverrideProxy const& _overriding, OverridePr ); } + if (_overriding.unimplemented() && !_super.unimplemented()) + { + solAssert(!_overriding.isVariable() || !_overriding.unimplemented(), ""); + overrideError( + _overriding, + _super, + 4593_error, + "Overriding an implemented " + _super.astNodeName() + + " with an unimplemented " + _overriding.astNodeName() + + " is not allowed." + ); + } + if (_super.isFunction()) { FunctionType const* functionType = _overriding.functionType(); @@ -613,14 +626,6 @@ void OverrideChecker::checkOverride(OverrideProxy const& _overriding, OverridePr stateMutabilityToString(_overriding.stateMutability()) + "\"." ); - - if (_overriding.unimplemented() && !_super.unimplemented()) - overrideError( - _overriding, - _super, - 4593_error, - "Overriding an implemented function with an unimplemented function is not allowed." - ); } } @@ -656,23 +661,21 @@ void OverrideChecker::overrideListError( ); } -void OverrideChecker::overrideError(Declaration const& _overriding, Declaration const& _super, ErrorId _error, string const& _message, string const& _secondaryMsg) +void OverrideChecker::overrideError( + OverrideProxy const& _overriding, + OverrideProxy const& _super, + ErrorId _error, + string const& _message, + optional const& _secondaryMsg +) { m_errorReporter.typeError( _error, _overriding.location(), - SecondarySourceLocation().append(_secondaryMsg, _super.location()), - _message - ); -} - - -void OverrideChecker::overrideError(OverrideProxy const& _overriding, OverrideProxy const& _super, ErrorId _error, string const& _message, string const& _secondaryMsg) -{ - m_errorReporter.typeError( - _error, - _overriding.location(), - SecondarySourceLocation().append(_secondaryMsg, _super.location()), + SecondarySourceLocation().append( + _secondaryMsg.value_or("Overridden " + _super.astNodeName() + " is here:"), + _super.location() + ), _message ); } diff --git a/libsolidity/analysis/OverrideChecker.h b/libsolidity/analysis/OverrideChecker.h index 564100453..9178d179d 100644 --- a/libsolidity/analysis/OverrideChecker.h +++ b/libsolidity/analysis/OverrideChecker.h @@ -166,19 +166,12 @@ private: std::string const& _message1, std::string const& _message2 ); - void overrideError( - Declaration const& _overriding, - Declaration const& _super, - langutil::ErrorId _error, - std::string const& _message, - std::string const& _secondaryMsg = "Overridden function is here:" - ); void overrideError( OverrideProxy const& _overriding, OverrideProxy const& _super, langutil::ErrorId _error, std::string const& _message, - std::string const& _secondaryMsg = "Overridden function is here:" + std::optional const& _secondaryMsg = {} ); /// Checks for functions in different base contracts which conflict with each /// other and thus need to be overridden explicitly. diff --git a/libsolidity/analysis/ReferencesResolver.cpp b/libsolidity/analysis/ReferencesResolver.cpp index aaa9c6ef9..abaeada03 100644 --- a/libsolidity/analysis/ReferencesResolver.cpp +++ b/libsolidity/analysis/ReferencesResolver.cpp @@ -201,9 +201,9 @@ bool ReferencesResolver::visit(Return const& _return) void ReferencesResolver::operator()(yul::FunctionDefinition const& _function) { - validateYulIdentifierName(_function.name, _function.location); + validateYulIdentifierName(_function.name, _function.debugData->location); for (yul::TypedName const& varName: _function.parameters + _function.returnVariables) - validateYulIdentifierName(varName.name, varName.location); + validateYulIdentifierName(varName.name, varName.debugData->location); bool wasInsideFunction = m_yulInsideFunction; m_yulInsideFunction = true; @@ -238,7 +238,7 @@ void ReferencesResolver::operator()(yul::Identifier const& _identifier) { m_errorReporter.declarationError( 4718_error, - _identifier.location, + _identifier.debugData->location, "Multiple matching identifiers. Resolving overloaded identifiers is not supported." ); return; @@ -251,7 +251,7 @@ void ReferencesResolver::operator()(yul::Identifier const& _identifier) ) m_errorReporter.declarationError( 9467_error, - _identifier.location, + _identifier.debugData->location, "Identifier not found. Use \".slot\" and \".offset\" to access storage variables." ); return; @@ -261,7 +261,7 @@ void ReferencesResolver::operator()(yul::Identifier const& _identifier) { m_errorReporter.declarationError( 6578_error, - _identifier.location, + _identifier.debugData->location, "Cannot access local Solidity variables from inside an inline assembly function." ); return; @@ -275,7 +275,7 @@ void ReferencesResolver::operator()(yul::VariableDeclaration const& _varDecl) { for (auto const& identifier: _varDecl.variables) { - validateYulIdentifierName(identifier.name, identifier.location); + validateYulIdentifierName(identifier.name, identifier.debugData->location); if ( @@ -289,7 +289,7 @@ void ReferencesResolver::operator()(yul::VariableDeclaration const& _varDecl) if (!ssl.infos.empty()) m_errorReporter.declarationError( 3859_error, - identifier.location, + identifier.debugData->location, ssl, "This declaration shadows a declaration outside the inline assembly block." ); diff --git a/libsolidity/analysis/SyntaxChecker.cpp b/libsolidity/analysis/SyntaxChecker.cpp index c85791449..04745344f 100644 --- a/libsolidity/analysis/SyntaxChecker.cpp +++ b/libsolidity/analysis/SyntaxChecker.cpp @@ -30,9 +30,6 @@ #include -#include - -#include #include using namespace std; diff --git a/libsolidity/analysis/TypeChecker.cpp b/libsolidity/analysis/TypeChecker.cpp index e0c288aeb..0c90d005c 100644 --- a/libsolidity/analysis/TypeChecker.cpp +++ b/libsolidity/analysis/TypeChecker.cpp @@ -489,8 +489,6 @@ bool TypeChecker::visit(FunctionDefinition const& _function) if (_function.isFallback()) typeCheckFallbackFunction(_function); - else if (_function.isReceive()) - typeCheckReceiveFunction(_function); else if (_function.isConstructor()) typeCheckConstructor(_function); @@ -588,7 +586,12 @@ bool TypeChecker::visit(VariableDeclaration const& _variable) if (result) { bool isLibraryStorageParameter = (_variable.isLibraryFunctionParameter() && referenceType->location() == DataLocation::Storage); - bool callDataCheckRequired = ((_variable.isConstructorParameter() || _variable.isPublicCallableParameter()) && !isLibraryStorageParameter); + // We skip the calldata check for abstract contract constructors. + bool isAbstractConstructorParam = _variable.isConstructorParameter() && m_currentContract && m_currentContract->abstract(); + bool callDataCheckRequired = + !isAbstractConstructorParam && + (_variable.isConstructorParameter() || _variable.isPublicCallableParameter()) && + !isLibraryStorageParameter; if (callDataCheckRequired) { if (!referenceType->interfaceType(false)) @@ -636,6 +639,15 @@ void TypeChecker::visitManually( "Can only use modifiers defined in the current contract or in base contracts." ); } + if ( + *_modifier.name().annotation().requiredLookup == VirtualLookup::Static && + !modifierDecl->isImplemented() + ) + m_errorReporter.typeError( + 1835_error, + _modifier.location(), + "Cannot call unimplemented modifier. The modifier has no implementation in the referenced contract. Refer to it by its unqualified name if you want to call the implementation from the most derived contract." + ); } else // check parameters for Base constructors @@ -716,7 +728,7 @@ void TypeChecker::endVisit(FunctionTypeName const& _funType) { solAssert(t->annotation().type, "Type not set for parameter."); if (!t->annotation().type->interfaceType(false).get()) - m_errorReporter.typeError(2582_error, t->location(), "Internal type cannot be used for external function type."); + m_errorReporter.fatalTypeError(2582_error, t->location(), "Internal type cannot be used for external function type."); } solAssert(fun.interfaceType(false), "External function type uses internal types."); } @@ -743,7 +755,7 @@ bool TypeChecker::visit(InlineAssembly const& _inlineAssembly) solAssert(var->type(), "Expected variable type!"); if (var->immutable()) { - m_errorReporter.typeError(3773_error, _identifier.location, "Assembly access to immutable variables is not supported."); + m_errorReporter.typeError(3773_error, _identifier.debugData->location, "Assembly access to immutable variables is not supported."); return false; } if (var->isConstant()) @@ -752,7 +764,7 @@ bool TypeChecker::visit(InlineAssembly const& _inlineAssembly) { m_errorReporter.typeError( 3558_error, - _identifier.location, + _identifier.debugData->location, "Constant variable is circular." ); return false; @@ -762,24 +774,24 @@ bool TypeChecker::visit(InlineAssembly const& _inlineAssembly) if (var && !var->value()) { - m_errorReporter.typeError(3224_error, _identifier.location, "Constant has no value."); + m_errorReporter.typeError(3224_error, _identifier.debugData->location, "Constant has no value."); return false; } else if (_context == yul::IdentifierContext::LValue) { - m_errorReporter.typeError(6252_error, _identifier.location, "Constant variables cannot be assigned to."); + m_errorReporter.typeError(6252_error, _identifier.debugData->location, "Constant variables cannot be assigned to."); return false; } else if (!identifierInfo.suffix.empty()) { - m_errorReporter.typeError(6617_error, _identifier.location, "The suffixes .offset and .slot can only be used on non-constant storage variables."); + m_errorReporter.typeError(6617_error, _identifier.debugData->location, "The suffixes .offset and .slot can only be used on non-constant storage variables."); return false; } else if (var && var->value() && !var->value()->annotation().type && !dynamic_cast(var->value().get())) { m_errorReporter.typeError( 2249_error, - _identifier.location, + _identifier.debugData->location, "Constant variables with non-literal values cannot be forward referenced from inline assembly." ); return false; @@ -789,7 +801,7 @@ bool TypeChecker::visit(InlineAssembly const& _inlineAssembly) type(*var->value())->category() != Type::Category::RationalNumber )) { - m_errorReporter.typeError(7615_error, _identifier.location, "Only direct number constants and references to such constants are supported by inline assembly."); + m_errorReporter.typeError(7615_error, _identifier.debugData->location, "Only direct number constants and references to such constants are supported by inline assembly."); return false; } } @@ -804,19 +816,19 @@ bool TypeChecker::visit(InlineAssembly const& _inlineAssembly) { if (suffix != "slot" && suffix != "offset") { - m_errorReporter.typeError(4656_error, _identifier.location, "State variables only support \".slot\" and \".offset\"."); + m_errorReporter.typeError(4656_error, _identifier.debugData->location, "State variables only support \".slot\" and \".offset\"."); return false; } else if (_context == yul::IdentifierContext::LValue) { if (var->isStateVariable()) { - m_errorReporter.typeError(4713_error, _identifier.location, "State variables cannot be assigned to - you have to use \"sstore()\"."); + m_errorReporter.typeError(4713_error, _identifier.debugData->location, "State variables cannot be assigned to - you have to use \"sstore()\"."); return false; } else if (suffix != "slot") { - m_errorReporter.typeError(9739_error, _identifier.location, "Only .slot can be assigned to."); + m_errorReporter.typeError(9739_error, _identifier.debugData->location, "Only .slot can be assigned to."); return false; } } @@ -828,13 +840,13 @@ bool TypeChecker::visit(InlineAssembly const& _inlineAssembly) { if (suffix != "offset" && suffix != "length") { - m_errorReporter.typeError(1536_error, _identifier.location, "Calldata variables only support \".offset\" and \".length\"."); + m_errorReporter.typeError(1536_error, _identifier.debugData->location, "Calldata variables only support \".offset\" and \".length\"."); return false; } } else { - m_errorReporter.typeError(3622_error, _identifier.location, "The suffix \"." + suffix + "\" is not supported by this variable or type."); + m_errorReporter.typeError(3622_error, _identifier.debugData->location, "The suffix \"." + suffix + "\" is not supported by this variable or type."); return false; } } @@ -842,14 +854,14 @@ bool TypeChecker::visit(InlineAssembly const& _inlineAssembly) { m_errorReporter.typeError( 1408_error, - _identifier.location, + _identifier.debugData->location, "Only local variables are supported. To access storage variables, use the \".slot\" and \".offset\" suffixes." ); return false; } else if (var->type()->dataStoredIn(DataLocation::Storage)) { - m_errorReporter.typeError(9068_error, _identifier.location, "You have to use the \".slot\" or \".offset\" suffix to access storage reference variables."); + m_errorReporter.typeError(9068_error, _identifier.debugData->location, "You have to use the \".slot\" or \".offset\" suffix to access storage reference variables."); return false; } else if (var->type()->sizeOnStack() != 1) @@ -858,18 +870,18 @@ bool TypeChecker::visit(InlineAssembly const& _inlineAssembly) auto const* arrayType = dynamic_cast(var->type()); arrayType && arrayType->isDynamicallySized() && arrayType->dataStoredIn(DataLocation::CallData) ) - m_errorReporter.typeError(1397_error, _identifier.location, "Call data elements cannot be accessed directly. Use \".offset\" and \".length\" to access the calldata offset and length of this array and then use \"calldatacopy\"."); + m_errorReporter.typeError(1397_error, _identifier.debugData->location, "Call data elements cannot be accessed directly. Use \".offset\" and \".length\" to access the calldata offset and length of this array and then use \"calldatacopy\"."); else { solAssert(!var->type()->dataStoredIn(DataLocation::CallData), ""); - m_errorReporter.typeError(9857_error, _identifier.location, "Only types that use one stack slot are supported."); + m_errorReporter.typeError(9857_error, _identifier.debugData->location, "Only types that use one stack slot are supported."); } return false; } } else if (!identifierInfo.suffix.empty()) { - m_errorReporter.typeError(7944_error, _identifier.location, "The suffixes \".offset\", \".slot\" and \".length\" can only be used with variables."); + m_errorReporter.typeError(7944_error, _identifier.debugData->location, "The suffixes \".offset\", \".slot\" and \".length\" can only be used with variables."); return false; } else if (_context == yul::IdentifierContext::LValue) @@ -877,7 +889,7 @@ bool TypeChecker::visit(InlineAssembly const& _inlineAssembly) if (dynamic_cast(declaration)) return false; - m_errorReporter.typeError(1990_error, _identifier.location, "Only local variables can be assigned to in inline assembly."); + m_errorReporter.typeError(1990_error, _identifier.debugData->location, "Only local variables can be assigned to in inline assembly."); return false; } @@ -886,7 +898,7 @@ bool TypeChecker::visit(InlineAssembly const& _inlineAssembly) solAssert(!!declaration->type(), "Type of declaration required but not yet determined."); if (dynamic_cast(declaration)) { - m_errorReporter.declarationError(2025_error, _identifier.location, "Access to functions is not allowed in inline assembly."); + m_errorReporter.declarationError(2025_error, _identifier.debugData->location, "Access to functions is not allowed in inline assembly."); return false; } else if (dynamic_cast(declaration)) @@ -896,7 +908,7 @@ bool TypeChecker::visit(InlineAssembly const& _inlineAssembly) { if (!contract->isLibrary()) { - m_errorReporter.typeError(4977_error, _identifier.location, "Expected a library."); + m_errorReporter.typeError(4977_error, _identifier.debugData->location, "Expected a library."); return false; } } @@ -1876,30 +1888,6 @@ void TypeChecker::typeCheckFallbackFunction(FunctionDefinition const& _function) } } -void TypeChecker::typeCheckReceiveFunction(FunctionDefinition const& _function) -{ - solAssert(_function.isReceive(), ""); - - if (_function.libraryFunction()) - m_errorReporter.typeError(4549_error, _function.location(), "Libraries cannot have receive ether functions."); - - if (_function.stateMutability() != StateMutability::Payable) - m_errorReporter.typeError( - 7793_error, - _function.location(), - "Receive ether function must be payable, but is \"" + - stateMutabilityToString(_function.stateMutability()) + - "\"." - ); - if (_function.visibility() != Visibility::External) - m_errorReporter.typeError(4095_error, _function.location(), "Receive ether function must be defined as \"external\"."); - if (!_function.returnParameters().empty()) - m_errorReporter.typeError(6899_error, _function.returnParameterList()->location(), "Receive ether function cannot return values."); - if (!_function.parameters().empty()) - m_errorReporter.typeError(6857_error, _function.parameterList().location(), "Receive ether function cannot take parameters."); -} - - void TypeChecker::typeCheckConstructor(FunctionDefinition const& _function) { solAssert(_function.isConstructor(), ""); @@ -2045,11 +2033,14 @@ void TypeChecker::typeCheckBytesConcatFunction( typeCheckFunctionGeneralChecks(_functionCall, _functionType); for (shared_ptr const& argument: _functionCall.arguments()) - if ( - Type const* argumentType = type(*argument); + { + Type const* argumentType = type(*argument); + bool notConvertibleToBytes = !argumentType->isImplicitlyConvertibleTo(*TypeProvider::fixedBytes(32)) && - !argumentType->isImplicitlyConvertibleTo(*TypeProvider::bytesMemory()) - ) + !argumentType->isImplicitlyConvertibleTo(*TypeProvider::bytesMemory()); + bool numberLiteral = (dynamic_cast(argumentType) != nullptr); + + if (notConvertibleToBytes || numberLiteral) m_errorReporter.typeError( 8015_error, argument->location(), @@ -2057,6 +2048,7 @@ void TypeChecker::typeCheckBytesConcatFunction( "bytes or fixed bytes type is required, but " + argumentType->toString(true) + " provided." ); + } } void TypeChecker::typeCheckFunctionGeneralChecks( @@ -3406,7 +3398,7 @@ void TypeChecker::checkErrorAndEventParameters(CallableDeclaration const& _calla for (ASTPointer const& var: _callable.parameters()) { if (type(*var)->containsNestedMapping()) - m_errorReporter.typeError( + m_errorReporter.fatalTypeError( 3448_error, var->location(), "Type containing a (nested) mapping is not allowed as " + kind + " parameter type." diff --git a/libsolidity/analysis/TypeChecker.h b/libsolidity/analysis/TypeChecker.h index e3714ddfc..bf5039204 100644 --- a/libsolidity/analysis/TypeChecker.h +++ b/libsolidity/analysis/TypeChecker.h @@ -96,7 +96,6 @@ private: ); void typeCheckFallbackFunction(FunctionDefinition const& _function); - void typeCheckReceiveFunction(FunctionDefinition const& _function); void typeCheckConstructor(FunctionDefinition const& _function); /// Performs general number and type checks of arguments against function call and struct ctor FunctionCall node parameters. diff --git a/libsolidity/analysis/ViewPureChecker.cpp b/libsolidity/analysis/ViewPureChecker.cpp index daf0f8625..87831972d 100644 --- a/libsolidity/analysis/ViewPureChecker.cpp +++ b/libsolidity/analysis/ViewPureChecker.cpp @@ -69,7 +69,7 @@ public: if (yul::EVMDialect const* dialect = dynamic_cast(&m_dialect)) if (yul::BuiltinFunctionForEVM const* fun = dialect->builtin(_funCall.functionName.name)) if (fun->instruction) - checkInstruction(_funCall.location, *fun->instruction); + checkInstruction(_funCall.debugData->location, *fun->instruction); for (auto const& arg: _funCall.arguments) std::visit(*this, arg); diff --git a/libsolidity/ast/AST.cpp b/libsolidity/ast/AST.cpp index 513906b81..6d5e89ce2 100644 --- a/libsolidity/ast/AST.cpp +++ b/libsolidity/ast/AST.cpp @@ -31,7 +31,6 @@ #include -#include #include #include @@ -57,6 +56,50 @@ Declaration const* ASTNode::referencedDeclaration(Expression const& _expression) return nullptr; } +FunctionDefinition const* ASTNode::resolveFunctionCall(FunctionCall const& _functionCall, ContractDefinition const* _mostDerivedContract) +{ + auto const* functionDef = dynamic_cast( + ASTNode::referencedDeclaration(_functionCall.expression()) + ); + + if (!functionDef) + return nullptr; + + if (auto const* memberAccess = dynamic_cast(&_functionCall.expression())) + { + if (*memberAccess->annotation().requiredLookup == VirtualLookup::Super) + { + if (auto const typeType = dynamic_cast(memberAccess->expression().annotation().type)) + if (auto const contractType = dynamic_cast(typeType->actualType())) + { + solAssert(_mostDerivedContract, ""); + solAssert(contractType->isSuper(), ""); + ContractDefinition const* superContract = contractType->contractDefinition().superContract(*_mostDerivedContract); + + return &functionDef->resolveVirtual( + *_mostDerivedContract, + superContract + ); + } + } + else + solAssert(*memberAccess->annotation().requiredLookup == VirtualLookup::Static, ""); + } + else if (auto const* identifier = dynamic_cast(&_functionCall.expression())) + { + solAssert(*identifier->annotation().requiredLookup == VirtualLookup::Virtual, ""); + if (functionDef->virtualSemantics()) + { + solAssert(_mostDerivedContract, ""); + return &functionDef->resolveVirtual(*_mostDerivedContract); + } + } + else + solAssert(false, ""); + + return functionDef; +} + ASTAnnotation& ASTNode::annotation() const { if (!m_annotation) @@ -276,6 +319,17 @@ FunctionDefinition const* ContractDefinition::nextConstructor(ContractDefinition return nullptr; } +multimap const& ContractDefinition::definedFunctionsByName() const +{ + return m_definedFunctionsByName.init([&]{ + std::multimap result; + for (FunctionDefinition const* fun: filteredNodes(m_subNodes)) + result.insert({fun->name(), fun}); + return result; + }); +} + + TypeNameAnnotation& TypeName::annotation() const { return initAnnotation(); @@ -397,6 +451,8 @@ FunctionDefinition const& FunctionDefinition::resolveVirtual( ) const { solAssert(!isConstructor(), ""); + solAssert(!name().empty(), ""); + // If we are not doing super-lookup and the function is not virtual, we can stop here. if (_searchStart == nullptr && !virtualSemantics()) return *this; @@ -407,19 +463,24 @@ FunctionDefinition const& FunctionDefinition::resolveVirtual( FunctionType const* functionType = TypeProvider::function(*this)->asExternallyCallableFunction(false); + bool foundSearchStart = (_searchStart == nullptr); for (ContractDefinition const* c: _mostDerivedContract.annotation().linearizedBaseContracts) { - if (_searchStart != nullptr && c != _searchStart) + if (!foundSearchStart && c != _searchStart) continue; - _searchStart = nullptr; - for (FunctionDefinition const* function: c->definedFunctions()) + else + foundSearchStart = true; + + for (FunctionDefinition const* function: c->definedFunctions(name())) if ( - function->name() == name() && - !function->isConstructor() && + // With super lookup analysis guarantees that there is an implemented function in the chain. + // With virtual lookup there are valid cases where returning an unimplemented one is fine. + (function->isImplemented() || _searchStart == nullptr) && FunctionType(*function).asExternallyCallableFunction(false)->hasEqualParameterTypes(*functionType) ) return *function; } + solAssert(false, "Virtual function " + name() + " not found."); return *this; // not reached } @@ -545,6 +606,18 @@ bool Declaration::isEventOrErrorParameter() const return dynamic_cast(scope()) || dynamic_cast(scope()); } +bool Declaration::isVisibleAsUnqualifiedName() const +{ + if (!scope()) + return true; + if (isStructMember() || isEnumValue() || isEventOrErrorParameter()) + return false; + if (auto const* functionDefinition = dynamic_cast(scope())) + if (!functionDefinition->isImplemented()) + return false; // parameter of a function without body + return true; +} + DeclarationAnnotation& Declaration::annotation() const { return initAnnotation(); @@ -696,8 +769,7 @@ set VariableDeclaration::allowedDataLocations() c if ( isConstructorParameter() || isInternalCallableParameter() || - isLibraryFunctionParameter() || - isTryCatchParameter() + isLibraryFunctionParameter() ) locations.insert(Location::Storage); if (!isTryCatchParameter() && !isConstructorParameter()) diff --git a/libsolidity/ast/AST.h b/libsolidity/ast/AST.h index 7a1609e81..f02e93599 100644 --- a/libsolidity/ast/AST.h +++ b/libsolidity/ast/AST.h @@ -36,6 +36,9 @@ #include +#include +#include + #include #include #include @@ -101,6 +104,8 @@ public: /// Extracts the referenced declaration from all nodes whose annotations support /// `referencedDeclaration`. static Declaration const* referencedDeclaration(Expression const& _expression); + /// Performs potential super or virtual lookup for a function call based on the most derived contract. + static FunctionDefinition const* resolveFunctionCall(FunctionCall const& _functionCall, ContractDefinition const* _mostDerivedContract); /// Returns the source code location of this node. SourceLocation const& location() const { return m_location; } @@ -260,6 +265,12 @@ public: /// @returns true if this is a declaration of a parameter of an event. bool isEventOrErrorParameter() const; + /// @returns false if the declaration can never be referenced without being qualified with a scope. + /// Usually the name alone can be used to refer to the corresponding entity. + /// But, for example, struct member names or enum member names always require a prefix. + /// Another example is event parameter names, which do not participate in any proper scope. + bool isVisibleAsUnqualifiedName() const; + /// @returns the type of expressions referencing this declaration. /// This can only be called once types of variable declarations have already been resolved. virtual Type const* type() const = 0; @@ -497,6 +508,13 @@ public: std::vector stateVariables() const { return filteredNodes(m_subNodes); } std::vector functionModifiers() const { return filteredNodes(m_subNodes); } std::vector definedFunctions() const { return filteredNodes(m_subNodes); } + /// @returns a view of all functions + /// defined in this contract of the given name (excluding inherited functions). + auto definedFunctions(std::string const& _name) const + { + auto&& [b, e] = definedFunctionsByName().equal_range(_name); + return ranges::subrange(b, e) | ranges::views::values; + } std::vector events() const { return filteredNodes(m_subNodes); } std::vector const& interfaceEvents() const; /// @returns all errors defined in this contract or any base contract @@ -546,6 +564,8 @@ public: FunctionDefinition const* nextConstructor(ContractDefinition const& _mostDerivedContract) const; private: + std::multimap const& definedFunctionsByName() const; + std::vector> m_baseContracts; std::vector> m_subNodes; ContractKind m_contractKind; @@ -553,6 +573,7 @@ private: util::LazyInit, FunctionTypePointer>>> m_interfaceFunctionList[2]; util::LazyInit> m_interfaceEvents; + util::LazyInit> m_definedFunctionsByName; }; /** diff --git a/libsolidity/ast/ASTJsonConverter.cpp b/libsolidity/ast/ASTJsonConverter.cpp index 70ec98992..ea8662010 100644 --- a/libsolidity/ast/ASTJsonConverter.cpp +++ b/libsolidity/ast/ASTJsonConverter.cpp @@ -43,7 +43,6 @@ #include #include -using namespace ranges; using namespace std; using namespace solidity::langutil; @@ -175,7 +174,7 @@ void ASTJsonConverter::appendExpressionAttributes( Json::Value ASTJsonConverter::inlineAssemblyIdentifierToJson(pair _info) const { Json::Value tuple(Json::objectValue); - tuple["src"] = sourceLocationToString(_info.first->location); + tuple["src"] = sourceLocationToString(_info.first->debugData->location); tuple["declaration"] = idOrNull(_info.second.declaration); tuple["isSlot"] = Json::Value(_info.second.suffix == "slot"); tuple["isOffset"] = Json::Value(_info.second.suffix == "offset"); @@ -271,7 +270,7 @@ bool ASTJsonConverter::visit(ContractDefinition const& _node) make_pair("contractKind", contractKind(_node.contractKind())), make_pair("abstract", _node.abstract()), make_pair("baseContracts", toJson(_node.baseContracts())), - make_pair("contractDependencies", getContainerIds(_node.annotation().contractDependencies | views::keys)), + make_pair("contractDependencies", getContainerIds(_node.annotation().contractDependencies | ranges::views::keys)), make_pair("usedErrors", getContainerIds(_node.interfaceErrors(false))), make_pair("nodes", toJson(_node.subNodes())), make_pair("scope", idOrNull(_node.scope())) diff --git a/libsolidity/ast/ASTJsonImporter.cpp b/libsolidity/ast/ASTJsonImporter.cpp index 0fa88ae3b..916dbac14 100644 --- a/libsolidity/ast/ASTJsonImporter.cpp +++ b/libsolidity/ast/ASTJsonImporter.cpp @@ -24,12 +24,10 @@ #include #include -#include #include #include #include -#include #include #include #include diff --git a/libsolidity/ast/Types.cpp b/libsolidity/ast/Types.cpp index c4466e398..06d6a0fd9 100644 --- a/libsolidity/ast/Types.cpp +++ b/libsolidity/ast/Types.cpp @@ -2613,23 +2613,6 @@ Type const* TupleType::mobileType() const return TypeProvider::tuple(move(mobiles)); } -Type const* TupleType::closestTemporaryType(Type const* _targetType) const -{ - solAssert(!!_targetType, ""); - TypePointers const& targetComponents = dynamic_cast(*_targetType).components(); - solAssert(components().size() == targetComponents.size(), ""); - TypePointers tempComponents(targetComponents.size()); - for (size_t i = 0; i < targetComponents.size(); ++i) - { - if (components()[i] && targetComponents[i]) - { - tempComponents[i] = components()[i]->closestTemporaryType(targetComponents[i]); - solAssert(tempComponents[i], ""); - } - } - return TypeProvider::tuple(move(tempComponents)); -} - FunctionType::FunctionType(FunctionDefinition const& _function, Kind _kind): m_kind(_kind), m_stateMutability(_function.stateMutability()), diff --git a/libsolidity/ast/Types.h b/libsolidity/ast/Types.h index 95aa3150f..50a8fb6ca 100644 --- a/libsolidity/ast/Types.h +++ b/libsolidity/ast/Types.h @@ -324,13 +324,6 @@ public: /// @returns true if this is a non-value type and the data of this type is stored at the /// given location. virtual bool dataStoredIn(DataLocation) const { return false; } - /// @returns the type of a temporary during assignment to a variable of the given type. - /// Specifically, returns the requested itself if it can be dynamically allocated (or is a value type) - /// and the mobile type otherwise. - virtual Type const* closestTemporaryType(Type const* _targetType) const - { - return _targetType->dataStoredIn(DataLocation::Storage) ? mobileType() : _targetType; - } /// Returns the list of all members of this type. Default implementation: no members apart from bound. /// @param _currentScope scope in which the members are accessed. @@ -600,7 +593,7 @@ public: private: rational m_value; - /// Bytes type to which the rational can be explicitly converted. + /// Bytes type to which the rational can be implicitly converted. /// Empty for all rationals that are not directly parsed from hex literals. Type const* m_compatibleBytesType; @@ -1103,8 +1096,6 @@ public: u256 storageSize() const override; bool hasSimpleZeroValueInMemory() const override { return false; } Type const* mobileType() const override; - /// Converts components to their temporary types and performs some wildcard matching. - Type const* closestTemporaryType(Type const* _targetType) const override; std::vector const& components() const { return m_components; } diff --git a/libsolidity/codegen/ABIFunctions.cpp b/libsolidity/codegen/ABIFunctions.cpp index ffced74c5..d964c1f14 100644 --- a/libsolidity/codegen/ABIFunctions.cpp +++ b/libsolidity/codegen/ABIFunctions.cpp @@ -205,12 +205,12 @@ string ABIFunctions::tupleDecoder(TypePointers const& _types, bool _fromMemory) Whiskers templ(R"( function (headStart, dataEnd) { - if slt(sub(dataEnd, headStart), ) { } + if slt(sub(dataEnd, headStart), ) { () } } )"); templ("functionName", functionName); - templ("revertString", revertReasonIfDebug("ABI decoding: tuple data too short")); + templ("revertString", revertReasonIfDebugFunction("ABI decoding: tuple data too short")); templ("minimumSize", to_string(headSize(decodingTypes))); string decodeElements; @@ -235,7 +235,7 @@ string ABIFunctions::tupleDecoder(TypePointers const& _types, bool _fromMemory) { let offset := (add(headStart, )) - if gt(offset, 0xffffffffffffffff) { } + if gt(offset, 0xffffffffffffffff) { () } let offset := @@ -244,7 +244,7 @@ string ABIFunctions::tupleDecoder(TypePointers const& _types, bool _fromMemory) )"); elementTempl("dynamic", decodingTypes[i]->isDynamicallyEncoded()); // TODO add test - elementTempl("revertString", revertReasonIfDebug("ABI decoding: invalid tuple offset")); + elementTempl("revertString", revertReasonIfDebugFunction("ABI decoding: invalid tuple offset")); elementTempl("load", _fromMemory ? "mload" : "calldataload"); elementTempl("values", boost::algorithm::join(valueNamesLocal, ", ")); elementTempl("pos", to_string(headPos)); @@ -487,12 +487,12 @@ string ABIFunctions::abiEncodingFunctionCalldataArrayWithoutCleanup( else templ("scaleLengthByStride", Whiskers(R"( - if gt(length, ) { } + if gt(length, ) { () } length := mul(length, ) )") ("stride", toCompactHexWithPrefix(fromArrayType.calldataStride())) ("maxLength", toCompactHexWithPrefix(u256(-1) / fromArrayType.calldataStride())) - ("revertString", revertReasonIfDebug("ABI encoding: array data too long")) + ("revertString", revertReasonIfDebugFunction("ABI encoding: array data too long")) .render() // TODO add revert test ); @@ -1148,14 +1148,14 @@ string ABIFunctions::abiDecodingFunctionArray(ArrayType const& _type, bool _from R"( // function (offset, end) -> array { - if iszero(slt(add(offset, 0x1f), end)) { } + if iszero(slt(add(offset, 0x1f), end)) { () } let length := array := (, length, end) } )" ); // TODO add test - templ("revertString", revertReasonIfDebug("ABI decoding: invalid calldata array offset")); + templ("revertString", revertReasonIfDebugFunction("ABI decoding: invalid calldata array offset")); templ("functionName", functionName); templ("readableTypeName", _type.toString(true)); templ("retrieveLength", _type.isDynamicallySized() ? (load + "(offset)") : toCompactHexWithPrefix(_type.length())); @@ -1188,13 +1188,13 @@ string ABIFunctions::abiDecodingFunctionArrayAvailableLength(ArrayType const& _t let src := offset if gt(add(src, mul(length, )), end) { - + () } for { let i := 0 } lt(i, length) { i := add(i, 1) } { let innerOffset := (src) - if gt(innerOffset, 0xffffffffffffffff) { } + if gt(innerOffset, 0xffffffffffffffff) { () } let elementPos := add(offset, innerOffset) let elementPos := src @@ -1215,9 +1215,9 @@ string ABIFunctions::abiDecodingFunctionArrayAvailableLength(ArrayType const& _t templ("dynamicBase", _type.baseType()->isDynamicallyEncoded()); templ( "revertInvalidStride", - revertReasonIfDebug("ABI decoding: invalid calldata array stride") + revertReasonIfDebugFunction("ABI decoding: invalid calldata array stride") ); - templ("revertStringOffset", revertReasonIfDebug("ABI decoding: invalid calldata array offset")); + templ("revertStringOffset", revertReasonIfDebugFunction("ABI decoding: invalid calldata array offset")); templ("decodingFun", abiDecodingFunction(*_type.baseType(), _fromMemory, false)); return templ.render(); }); @@ -1241,15 +1241,15 @@ string ABIFunctions::abiDecodingFunctionCalldataArray(ArrayType const& _type) w = Whiskers(R"( // function (offset, end) -> arrayPos, length { - if iszero(slt(add(offset, 0x1f), end)) { } + if iszero(slt(add(offset, 0x1f), end)) { () } length := calldataload(offset) - if gt(length, 0xffffffffffffffff) { } + if gt(length, 0xffffffffffffffff) { () } arrayPos := add(offset, 0x20) - if gt(add(arrayPos, mul(length, )), end) { } + if gt(add(arrayPos, mul(length, )), end) { () } } )"); - w("revertStringOffset", revertReasonIfDebug("ABI decoding: invalid calldata array offset")); - w("revertStringLength", revertReasonIfDebug("ABI decoding: invalid calldata array length")); + w("revertStringOffset", revertReasonIfDebugFunction("ABI decoding: invalid calldata array offset")); + w("revertStringLength", revertReasonIfDebugFunction("ABI decoding: invalid calldata array length")); } else { @@ -1257,12 +1257,12 @@ string ABIFunctions::abiDecodingFunctionCalldataArray(ArrayType const& _type) // function (offset, end) -> arrayPos { arrayPos := offset - if gt(add(arrayPos, mul(, )), end) { } + if gt(add(arrayPos, mul(, )), end) { () } } )"); w("length", toCompactHexWithPrefix(_type.length())); } - w("revertStringPos", revertReasonIfDebug("ABI decoding: invalid calldata array stride")); + w("revertStringPos", revertReasonIfDebugFunction("ABI decoding: invalid calldata array stride")); w("functionName", functionName); w("readableTypeName", _type.toString(true)); w("stride", toCompactHexWithPrefix(_type.calldataStride())); @@ -1288,11 +1288,11 @@ string ABIFunctions::abiDecodingFunctionByteArrayAvailableLength(ArrayType const array := ((length)) mstore(array, length) let dst := add(array, 0x20) - if gt(add(src, length), end) { } + if gt(add(src, length), end) { () } (src, dst, length) } )"); - templ("revertStringLength", revertReasonIfDebug("ABI decoding: invalid byte array length")); + templ("revertStringLength", revertReasonIfDebugFunction("ABI decoding: invalid byte array length")); templ("functionName", functionName); templ("allocate", m_utils.allocationFunction()); templ("allocationSize", m_utils.arrayAllocationSizeFunction(_type)); @@ -1312,12 +1312,12 @@ string ABIFunctions::abiDecodingFunctionCalldataStruct(StructType const& _type) Whiskers w{R"( // function (offset, end) -> value { - if slt(sub(end, offset), ) { } + if slt(sub(end, offset), ) { () } value := offset } )"}; // TODO add test - w("revertString", revertReasonIfDebug("ABI decoding: struct calldata too short")); + w("revertString", revertReasonIfDebugFunction("ABI decoding: struct calldata too short")); w("functionName", functionName); w("readableTypeName", _type.toString(true)); w("minimumSize", to_string(_type.isDynamicallyEncoded() ? _type.calldataEncodedTailSize() : _type.calldataEncodedSize(true))); @@ -1337,7 +1337,7 @@ string ABIFunctions::abiDecodingFunctionStruct(StructType const& _type, bool _fr Whiskers templ(R"( // function (headStart, end) -> value { - if slt(sub(end, headStart), ) { } + if slt(sub(end, headStart), ) { () } value := () <#members> { @@ -1348,7 +1348,7 @@ string ABIFunctions::abiDecodingFunctionStruct(StructType const& _type, bool _fr } )"); // TODO add test - templ("revertString", revertReasonIfDebug("ABI decoding: struct data too short")); + templ("revertString", revertReasonIfDebugFunction("ABI decoding: struct data too short")); templ("functionName", functionName); templ("readableTypeName", _type.toString(true)); templ("allocate", m_utils.allocationFunction()); @@ -1365,7 +1365,7 @@ string ABIFunctions::abiDecodingFunctionStruct(StructType const& _type, bool _fr Whiskers memberTempl(R"( let offset := (add(headStart, )) - if gt(offset, 0xffffffffffffffff) { } + if gt(offset, 0xffffffffffffffff) { () } let offset := @@ -1373,7 +1373,7 @@ string ABIFunctions::abiDecodingFunctionStruct(StructType const& _type, bool _fr )"); memberTempl("dynamic", decodingType->isDynamicallyEncoded()); // TODO add test - memberTempl("revertString", revertReasonIfDebug("ABI decoding: invalid struct offset")); + memberTempl("revertString", revertReasonIfDebugFunction("ABI decoding: invalid struct offset")); memberTempl("load", _fromMemory ? "mload" : "calldataload"); memberTempl("pos", to_string(headPos)); memberTempl("memoryOffset", toCompactHexWithPrefix(_type.memoryOffsetOfMember(member.name))); @@ -1441,7 +1441,7 @@ string ABIFunctions::calldataAccessFunction(Type const& _type) Whiskers w(R"( function (base_ref, ptr) -> { let rel_offset_of_tail := calldataload(ptr) - if iszero(slt(rel_offset_of_tail, sub(sub(calldatasize(), base_ref), sub(, 1)))) { } + if iszero(slt(rel_offset_of_tail, sub(sub(calldatasize(), base_ref), sub(, 1)))) { () } value := add(rel_offset_of_tail, base_ref) } @@ -1453,14 +1453,14 @@ string ABIFunctions::calldataAccessFunction(Type const& _type) w("handleLength", Whiskers(R"( length := calldataload(value) value := add(value, 0x20) - if gt(length, 0xffffffffffffffff) { } - if sgt(base_ref, sub(calldatasize(), mul(length, ))) { } + if gt(length, 0xffffffffffffffff) { () } + if sgt(base_ref, sub(calldatasize(), mul(length, ))) { () } )") ("calldataStride", toCompactHexWithPrefix(arrayType->calldataStride())) // TODO add test - ("revertStringLength", revertReasonIfDebug("Invalid calldata access length")) + ("revertStringLength", revertReasonIfDebugFunction("Invalid calldata access length")) // TODO add test - ("revertStringStride", revertReasonIfDebug("Invalid calldata access stride")) + ("revertStringStride", revertReasonIfDebugFunction("Invalid calldata access stride")) .render()); w("return", "value, length"); } @@ -1471,7 +1471,7 @@ string ABIFunctions::calldataAccessFunction(Type const& _type) } w("neededLength", toCompactHexWithPrefix(tailSize)); w("functionName", functionName); - w("revertStringOffset", revertReasonIfDebug("Invalid calldata access offset")); + w("revertStringOffset", revertReasonIfDebugFunction("Invalid calldata access offset")); return w.render(); } else if (_type.isValueType()) @@ -1555,7 +1555,7 @@ size_t ABIFunctions::numVariablesForType(Type const& _type, EncodingOptions cons return _type.sizeOnStack(); } -std::string ABIFunctions::revertReasonIfDebug(std::string const& _message) +std::string ABIFunctions::revertReasonIfDebugFunction(std::string const& _message) { - return YulUtilFunctions::revertReasonIfDebug(m_revertStrings, _message); + return m_utils.revertReasonIfDebugFunction(_message); } diff --git a/libsolidity/codegen/ABIFunctions.h b/libsolidity/codegen/ABIFunctions.h index 8cd858ba0..07088aa3b 100644 --- a/libsolidity/codegen/ABIFunctions.h +++ b/libsolidity/codegen/ABIFunctions.h @@ -273,9 +273,9 @@ private: /// is true), for which it is two. static size_t numVariablesForType(Type const& _type, EncodingOptions const& _options); - /// @returns code that stores @param _message for revert reason + /// @returns the name of a function that uses @param _message for revert reason /// if m_revertStrings is debug. - std::string revertReasonIfDebug(std::string const& _message = ""); + std::string revertReasonIfDebugFunction(std::string const& _message = ""); langutil::EVMVersion m_evmVersion; RevertStrings const m_revertStrings; diff --git a/libsolidity/codegen/Compiler.cpp b/libsolidity/codegen/Compiler.cpp index 0b6385943..e3308b2de 100644 --- a/libsolidity/codegen/Compiler.cpp +++ b/libsolidity/codegen/Compiler.cpp @@ -38,7 +38,7 @@ void Compiler::compileContract( { ContractCompiler runtimeCompiler(nullptr, m_runtimeContext, m_optimiserSettings); runtimeCompiler.compileContract(_contract, _otherCompilers); - m_runtimeContext.appendAuxiliaryData(_metadata); + m_runtimeContext.appendToAuxiliaryData(_metadata); // This might modify m_runtimeContext because it can access runtime functions at // creation time. diff --git a/libsolidity/codegen/Compiler.h b/libsolidity/codegen/Compiler.h index 6e6fd968e..0d021b723 100644 --- a/libsolidity/codegen/Compiler.h +++ b/libsolidity/codegen/Compiler.h @@ -31,7 +31,8 @@ #include #include -namespace solidity::frontend { +namespace solidity::frontend +{ class Compiler { diff --git a/libsolidity/codegen/CompilerContext.cpp b/libsolidity/codegen/CompilerContext.cpp index cddeea472..7b673cca6 100644 --- a/libsolidity/codegen/CompilerContext.cpp +++ b/libsolidity/codegen/CompilerContext.cpp @@ -28,11 +28,11 @@ #include #include +#include #include #include #include #include -#include #include #include #include @@ -48,10 +48,7 @@ #include #include -#include - #include -#include // Change to "define" to output all intermediate code #undef SOL_OUTPUT_ASM @@ -148,7 +145,7 @@ void CompilerContext::callYulFunction( m_externallyUsedYulFunctions.insert(_name); auto const retTag = pushNewTag(); CompilerUtils(*this).moveIntoStack(_inArgs); - appendJumpTo(namedTag(_name), evmasm::AssemblyItem::JumpType::IntoFunction); + appendJumpTo(namedTag(_name, _inArgs, _outArgs, {}), evmasm::AssemblyItem::JumpType::IntoFunction); adjustStackOffset(static_cast(_outArgs) - 1 - static_cast(_inArgs)); *this << retTag.tag(); } @@ -286,7 +283,11 @@ FunctionDefinition const& CompilerContext::superFunction(FunctionDefinition cons solAssert(m_mostDerivedContract, "No most derived contract set."); ContractDefinition const* super = _base.superContract(mostDerivedContract()); solAssert(super, "Super contract not available."); - return _function.resolveVirtual(mostDerivedContract(), super); + + FunctionDefinition const& resolvedFunction = _function.resolveVirtual(mostDerivedContract(), super); + solAssert(resolvedFunction.isImplemented(), ""); + + return resolvedFunction; } ContractDefinition const& CompilerContext::mostDerivedContract() const @@ -334,14 +335,7 @@ CompilerContext& CompilerContext::appendJump(evmasm::AssemblyItem::JumpType _jum CompilerContext& CompilerContext::appendPanic(util::PanicCode _code) { - Whiskers templ(R"({ - mstore(0, ) - mstore(4, ) - revert(0, 0x24) - })"); - templ("selector", util::selectorFromSignature("Panic(uint256)").str()); - templ("code", u256(_code).str()); - appendInlineAssembly(templ.render()); + callYulFunction(utilFunctions().panicFunction(_code), 0, 0); return *this; } @@ -426,7 +420,7 @@ void CompilerContext::appendInlineAssembly( if (stackDiff < 1 || stackDiff > 16) BOOST_THROW_EXCEPTION( StackTooDeepError() << - errinfo_sourceLocation(_identifier.location) << + errinfo_sourceLocation(_identifier.debugData->location) << util::errinfo_comment("Stack too deep (" + to_string(stackDiff) + "), try removing local variables.") ); if (_context == yul::IdentifierContext::RValue) @@ -560,7 +554,11 @@ void CompilerContext::optimizeYul(yul::Object& _object, yul::EVMDialect const& _ string CompilerContext::revertReasonIfDebug(string const& _message) { - return YulUtilFunctions::revertReasonIfDebug(m_revertStrings, _message); + return YulUtilFunctions::revertReasonIfDebugBody( + m_revertStrings, + "mload(" + to_string(CompilerUtils::freeMemoryPointer) + ")", + _message + ); } void CompilerContext::updateSourceLocation() @@ -592,7 +590,23 @@ evmasm::AssemblyItem CompilerContext::FunctionCompilationQueue::entryLabel( auto res = m_entryLabels.find(&_declaration); if (res == m_entryLabels.end()) { - evmasm::AssemblyItem tag(_context.newTag()); + size_t params = 0; + size_t returns = 0; + if (auto const* function = dynamic_cast(&_declaration)) + { + FunctionType functionType(*function, FunctionType::Kind::Internal); + params = CompilerUtils::sizeOnStack(functionType.parameterTypes()); + returns = CompilerUtils::sizeOnStack(functionType.returnParameterTypes()); + } + + // some name that cannot clash with yul function names. + string labelName = "@" + _declaration.name() + "_" + to_string(_declaration.id()); + evmasm::AssemblyItem tag = _context.namedTag( + labelName, + params, + returns, + _declaration.id() + ); m_entryLabels.insert(make_pair(&_declaration, tag)); m_functionsToCompile.push(&_declaration); return tag.tag(); diff --git a/libsolidity/codegen/CompilerContext.h b/libsolidity/codegen/CompilerContext.h index 1ec620271..b36397864 100644 --- a/libsolidity/codegen/CompilerContext.h +++ b/libsolidity/codegen/CompilerContext.h @@ -47,7 +47,8 @@ #include #include -namespace solidity::frontend { +namespace solidity::frontend +{ class Compiler; @@ -216,7 +217,10 @@ public: /// @returns a new tag without pushing any opcodes or data evmasm::AssemblyItem newTag() { return m_asm->newTag(); } /// @returns a new tag identified by name. - evmasm::AssemblyItem namedTag(std::string const& _name) { return m_asm->namedTag(_name); } + evmasm::AssemblyItem namedTag(std::string const& _name, size_t _params, size_t _returns, std::optional _sourceID) + { + return m_asm->namedTag(_name, _params, _returns, _sourceID); + } /// Adds a subroutine to the code (in the data section) and pushes its size (via a tag) /// on the stack. @returns the pushsub assembly item. evmasm::AssemblyItem addSubroutine(evmasm::AssemblyPointer const& _assembly) { return m_asm->appendSubroutine(_assembly); } @@ -268,14 +272,14 @@ public: ); /// If m_revertStrings is debug, @returns inline assembly code that - /// stores @param _message in memory position 0 and reverts. + /// stores @param _message at the free memory pointer and reverts. /// Otherwise returns "revert(0, 0)". std::string revertReasonIfDebug(std::string const& _message = ""); void optimizeYul(yul::Object& _object, yul::EVMDialect const& _dialect, OptimiserSettings const& _optimiserSetting, std::set const& _externalIdentifiers = {}); /// Appends arbitrary data to the end of the bytecode. - void appendAuxiliaryData(bytes const& _data) { m_asm->appendAuxiliaryDataToEnd(_data); } + void appendToAuxiliaryData(bytes const& _data) { m_asm->appendToAuxiliaryData(_data); } /// Run optimisation step. void optimise(OptimiserSettings const& _settings) { m_asm->optimise(translateOptimiserSettings(_settings)); } diff --git a/libsolidity/codegen/CompilerUtils.cpp b/libsolidity/codegen/CompilerUtils.cpp index 08180be4e..9ae1741dd 100644 --- a/libsolidity/codegen/CompilerUtils.cpp +++ b/libsolidity/codegen/CompilerUtils.cpp @@ -1078,8 +1078,7 @@ void CompilerUtils::convertType( } case DataLocation::CallData: solAssert( - targetType.isByteArray() && - typeOnStack.isByteArray() && + ((targetType.isByteArray() && typeOnStack.isByteArray()) || _typeOnStack == _targetType) && typeOnStack.location() == DataLocation::CallData, "Invalid conversion to calldata type." ); @@ -1130,9 +1129,6 @@ void CompilerUtils::convertType( solAssert(targetTypeCategory == stackTypeCategory, ""); auto& targetType = dynamic_cast(_targetType); auto& typeOnStack = dynamic_cast(_typeOnStack); - solAssert( - targetType.location() != DataLocation::CallData - , ""); switch (targetType.location()) { case DataLocation::Storage: @@ -1208,7 +1204,8 @@ void CompilerUtils::convertType( } break; case DataLocation::CallData: - solAssert(false, "Invalid type conversion target location CallData."); + solAssert(_typeOnStack == _targetType, ""); + // nothing to do break; } break; diff --git a/libsolidity/codegen/CompilerUtils.h b/libsolidity/codegen/CompilerUtils.h index 0930ff1c8..721f65614 100644 --- a/libsolidity/codegen/CompilerUtils.h +++ b/libsolidity/codegen/CompilerUtils.h @@ -29,7 +29,8 @@ #include #include -namespace solidity::frontend { +namespace solidity::frontend +{ class Type; // forward diff --git a/libsolidity/codegen/ContractCompiler.cpp b/libsolidity/codegen/ContractCompiler.cpp index 53b18419d..fca076b65 100644 --- a/libsolidity/codegen/ContractCompiler.cpp +++ b/libsolidity/codegen/ContractCompiler.cpp @@ -237,15 +237,18 @@ size_t ContractCompiler::deployLibrary(ContractDefinition const& _contract) codecopy(codepos, subOffset, subSize) // Check that the first opcode is a PUSH20 if iszero(eq(0x73, byte(0, mload(codepos)))) { - mstore(0, ) - mstore(4, 0) + mstore(0, ) + mstore(4, ) revert(0, 0x24) } mstore(0, address()) mstore8(codepos, 0x73) return(codepos, subSize) } - )")("panicSig", util::selectorFromSignature("Panic(uint256)").str()).render(), + )") + ("panicSelector", util::selectorFromSignature("Panic(uint256)").str()) + ("panicCode", "0") + .render(), {"subSize", "subOffset"} ); @@ -601,6 +604,8 @@ bool ContractCompiler::visit(VariableDeclaration const& _variableDeclaration) bool ContractCompiler::visit(FunctionDefinition const& _function) { + solAssert(_function.isImplemented(), ""); + CompilerContext::LocationSetter locationSetter(m_context, _function); m_context.startFunction(_function); @@ -1351,6 +1356,7 @@ bool ContractCompiler::visit(PlaceholderStatement const& _placeholderStatement) bool ContractCompiler::visit(Block const& _block) { + m_context.pushVisitedNodes(&_block); if (_block.unchecked()) { solAssert(m_context.arithmetic() == Arithmetic::Checked, ""); @@ -1369,6 +1375,7 @@ void ContractCompiler::endVisit(Block const& _block) } // Frees local variables declared in the scope of this block. popScopedVariables(&_block); + m_context.popVisitedNodes(); } void ContractCompiler::appendMissingFunctions() diff --git a/libsolidity/codegen/ExpressionCompiler.cpp b/libsolidity/codegen/ExpressionCompiler.cpp index a8e7dd5b7..338430723 100644 --- a/libsolidity/codegen/ExpressionCompiler.cpp +++ b/libsolidity/codegen/ExpressionCompiler.cpp @@ -49,6 +49,34 @@ using namespace solidity::frontend; using namespace solidity::langutil; using namespace solidity::util; +namespace +{ + +Type const* closestType(Type const* _type, Type const* _targetType, bool _isShiftOp) +{ + if (_isShiftOp) + return _type->mobileType(); + else if (auto const* tupleType = dynamic_cast(_type)) + { + solAssert(_targetType, ""); + TypePointers const& targetComponents = dynamic_cast(*_targetType).components(); + solAssert(tupleType->components().size() == targetComponents.size(), ""); + TypePointers tempComponents(targetComponents.size()); + for (size_t i = 0; i < targetComponents.size(); ++i) + { + if (tupleType->components()[i] && targetComponents[i]) + { + tempComponents[i] = closestType(tupleType->components()[i], targetComponents[i], _isShiftOp); + solAssert(tempComponents[i], ""); + } + } + return TypeProvider::tuple(move(tempComponents)); + } + else + return _targetType->dataStoredIn(DataLocation::Storage) ? _type->mobileType() : _targetType; +} + +} void ExpressionCompiler::compile(Expression const& _expression) { @@ -280,13 +308,12 @@ bool ExpressionCompiler::visit(Assignment const& _assignment) _assignment.rightHandSide().accept(*this); // Perform some conversion already. This will convert storage types to memory and literals // to their actual type, but will not convert e.g. memory to storage. - Type const* rightIntermediateType; - if (op != Token::Assign && TokenTraits::isShiftOp(binOp)) - rightIntermediateType = _assignment.rightHandSide().annotation().type->mobileType(); - else - rightIntermediateType = _assignment.rightHandSide().annotation().type->closestTemporaryType( - _assignment.leftHandSide().annotation().type - ); + Type const* rightIntermediateType = closestType( + _assignment.rightHandSide().annotation().type, + _assignment.leftHandSide().annotation().type, + op != Token::Assign && TokenTraits::isShiftOp(binOp) + ); + solAssert(rightIntermediateType, ""); utils().convertType(*_assignment.rightHandSide().annotation().type, *rightIntermediateType, cleanupNeeded); @@ -1016,7 +1043,10 @@ bool ExpressionCompiler::visit(FunctionCall const& _functionCall) // stack: argValue storageSlot slotOffset utils().moveToStackTop(2, argType->sizeOnStack()); // stack: storageSlot slotOffset argValue - Type const* type = arguments[0]->annotation().type->closestTemporaryType(arrayType->baseType()); + Type const* type = + arrayType->baseType()->dataStoredIn(DataLocation::Storage) ? + arguments[0]->annotation().type->mobileType() : + arrayType->baseType(); solAssert(type, ""); utils().convertType(*argType, *type); utils().moveToStackTop(1 + type->sizeOnStack()); @@ -1053,11 +1083,12 @@ bool ExpressionCompiler::visit(FunctionCall const& _functionCall) targetTypes.emplace_back(argument->annotation().type); else if ( auto const* literalType = dynamic_cast(argument->annotation().type); - literalType && literalType->value().size() <= 32 + literalType && !literalType->value().empty() && literalType->value().size() <= 32 ) targetTypes.emplace_back(TypeProvider::fixedBytes(static_cast(literalType->value().size()))); else { + solAssert(!dynamic_cast(argument->annotation().type), ""); solAssert(argument->annotation().type->isImplicitlyConvertibleTo(*TypeProvider::bytesMemory()), ""); targetTypes.emplace_back(TypeProvider::bytesMemory()); } diff --git a/libsolidity/codegen/ExpressionCompiler.h b/libsolidity/codegen/ExpressionCompiler.h index 57ba34549..b583b8327 100644 --- a/libsolidity/codegen/ExpressionCompiler.h +++ b/libsolidity/codegen/ExpressionCompiler.h @@ -39,7 +39,8 @@ namespace solidity::evmasm class AssemblyItem; // forward } -namespace solidity::frontend { +namespace solidity::frontend +{ // forward declarations class CompilerContext; diff --git a/libsolidity/codegen/LValue.cpp b/libsolidity/codegen/LValue.cpp index 0b999e0c4..e7e069352 100644 --- a/libsolidity/codegen/LValue.cpp +++ b/libsolidity/codegen/LValue.cpp @@ -300,16 +300,26 @@ void StorageItem::storeValue(Type const& _sourceType, SourceLocation const& _loc // stack: value storage_ref cleared_value multiplier utils.copyToStackTop(3 + m_dataType->sizeOnStack(), m_dataType->sizeOnStack()); // stack: value storage_ref cleared_value multiplier value - if (FunctionType const* fun = dynamic_cast(m_dataType)) + if (auto const* fun = dynamic_cast(m_dataType)) { - solAssert(_sourceType == *m_dataType, "function item stored but target is not equal to source"); + solAssert( + _sourceType.isImplicitlyConvertibleTo(*m_dataType), + "function item stored but target is not implicitly convertible to source" + ); + solAssert(!fun->bound(), ""); if (fun->kind() == FunctionType::Kind::External) + { + solAssert(fun->sizeOnStack() == 2, ""); // Combine the two-item function type into a single stack slot. utils.combineExternalFunctionType(false); + } else + { + solAssert(fun->sizeOnStack() == 1, ""); m_context << ((u256(1) << (8 * m_dataType->storageBytes())) - 1) << Instruction::AND; + } } else if (m_dataType->category() == Type::Category::FixedBytes) { diff --git a/libsolidity/codegen/YulUtilFunctions.cpp b/libsolidity/codegen/YulUtilFunctions.cpp index 5134822bb..d7c31a72b 100644 --- a/libsolidity/codegen/YulUtilFunctions.cpp +++ b/libsolidity/codegen/YulUtilFunctions.cpp @@ -152,6 +152,54 @@ string YulUtilFunctions::storeLiteralInMemoryFunction(string const& _literal) }); } +string YulUtilFunctions::copyLiteralToStorageFunction(string const& _literal) +{ + string functionName = "copy_literal_to_storage_" + util::toHex(util::keccak256(_literal).asBytes()); + + return m_functionCollector.createFunction(functionName, [&](vector& _args, vector&) { + _args = {"slot"}; + + if (_literal.size() >= 32) + { + size_t words = (_literal.length() + 31) / 32; + vector> wordParams(words); + for (size_t i = 0; i < words; ++i) + { + wordParams[i]["offset"] = to_string(i); + wordParams[i]["wordValue"] = formatAsStringOrNumber(_literal.substr(32 * i, 32)); + } + return Whiskers(R"( + let oldLen := (sload(slot)) + (slot, oldLen, ) + sstore(slot, ) + let dstPtr := (slot) + <#word> + sstore(add(dstPtr, ), ) + + )") + ("byteArrayLength", extractByteArrayLengthFunction()) + ("cleanUpArrayEnd", cleanUpDynamicByteArrayEndSlotsFunction(*TypeProvider::bytesStorage())) + ("dataArea", arrayDataAreaFunction(*TypeProvider::bytesStorage())) + ("word", wordParams) + ("length", to_string(_literal.size())) + ("encodedLen", to_string(2 * _literal.size() + 1)) + .render(); + } + else + return Whiskers(R"( + let oldLen := (sload(slot)) + (slot, oldLen, ) + sstore(slot, add(, )) + )") + ("byteArrayLength", extractByteArrayLengthFunction()) + ("cleanUpArrayEnd", cleanUpDynamicByteArrayEndSlotsFunction(*TypeProvider::bytesStorage())) + ("wordValue", formatAsStringOrNumber(_literal)) + ("length", to_string(_literal.size())) + ("encodedLen", to_string(2 * _literal.size())) + .render(); + }); +} + string YulUtilFunctions::requireOrAssertFunction(bool _assert, Type const* _messageType) { string functionName = @@ -541,6 +589,18 @@ string YulUtilFunctions::roundUpFunction() }); } +string YulUtilFunctions::divide32CeilFunction() +{ + return m_functionCollector.createFunction( + "divide_by_32_ceil", + [&](vector& _args, vector& _ret) { + _args = {"value"}; + _ret = {"result"}; + return "result := div(add(value, 31), 32)"; + } + ); +} + string YulUtilFunctions::overflowCheckedIntAddFunction(IntegerType const& _type) { string functionName = "checked_add_" + _type.identifier(); @@ -1168,21 +1228,7 @@ std::string YulUtilFunctions::resizeArrayFunction(ArrayType const& _type) - // Size was reduced, clear end of array - if lt(newLen, oldLen) { - let oldSlotCount := (oldLen) - let newSlotCount := (newLen) - let arrayDataStart := (array) - let deleteStart := add(arrayDataStart, newSlotCount) - let deleteEnd := add(arrayDataStart, oldSlotCount) - - // if we are dealing with packed array and offset is greater than zero - // we have to partially clear last slot that is still used, so decreasing start by one - let offset := mul(mod(newLen, ), ) - if gt(offset, 0) { (sub(deleteStart, 1), offset) } - - (deleteStart, deleteEnd) - } + (array, oldLen, newLen) })"); templ("functionName", functionName); @@ -1193,19 +1239,49 @@ std::string YulUtilFunctions::resizeArrayFunction(ArrayType const& _type) bool isMappingBase = _type.baseType()->category() == Type::Category::Mapping; templ("needsClearing", !isMappingBase); if (!isMappingBase) - { - templ("convertToSize", arrayConvertLengthToSize(_type)); - templ("dataPosition", arrayDataAreaFunction(_type)); - templ("clearStorageRange", clearStorageRangeFunction(*_type.baseType())); - templ("packed", _type.baseType()->storageBytes() <= 16); - templ("itemsPerSlot", to_string(32 / _type.baseType()->storageBytes())); - templ("storageBytes", to_string(_type.baseType()->storageBytes())); - templ("partialClearStorageSlot", partialClearStorageSlotFunction()); - } + templ("cleanUpArrayEnd", cleanUpStorageArrayEndFunction(_type)); return templ.render(); }); } +string YulUtilFunctions::cleanUpStorageArrayEndFunction(ArrayType const& _type) +{ + solAssert(_type.location() == DataLocation::Storage, ""); + solAssert(_type.baseType()->category() != Type::Category::Mapping, ""); + solAssert(!_type.isByteArray(), ""); + solUnimplementedAssert(_type.baseType()->storageBytes() <= 32, ""); + + string functionName = "cleanup_storage_array_end_" + _type.identifier(); + return m_functionCollector.createFunction(functionName, [&](vector& _args, vector&) { + _args = {"array", "len", "startIndex"}; + return Whiskers(R"( + if lt(startIndex, len) { + // Size was reduced, clear end of array + let oldSlotCount := (len) + let newSlotCount := (startIndex) + let arrayDataStart := (array) + let deleteStart := add(arrayDataStart, newSlotCount) + let deleteEnd := add(arrayDataStart, oldSlotCount) + + // if we are dealing with packed array and offset is greater than zero + // we have to partially clear last slot that is still used, so decreasing start by one + let offset := mul(mod(startIndex, ), ) + if gt(offset, 0) { (sub(deleteStart, 1), offset) } + + (deleteStart, deleteEnd) + } + )") + ("convertToSize", arrayConvertLengthToSize(_type)) + ("dataPosition", arrayDataAreaFunction(_type)) + ("clearStorageRange", clearStorageRangeFunction(*_type.baseType())) + ("packed", _type.baseType()->storageBytes() <= 16) + ("itemsPerSlot", to_string(32 / _type.baseType()->storageBytes())) + ("storageBytes", to_string(_type.baseType()->storageBytes())) + ("partialClearStorageSlot", partialClearStorageSlotFunction()) + .render(); + }); +} + string YulUtilFunctions::resizeDynamicByteArrayFunction(ArrayType const& _type) { string functionName = "resize_array_" + _type.identifier(); @@ -1230,6 +1306,30 @@ string YulUtilFunctions::resizeDynamicByteArrayFunction(ArrayType const& _type) }); } +string YulUtilFunctions::cleanUpDynamicByteArrayEndSlotsFunction(ArrayType const& _type) +{ + solAssert(_type.isByteArray(), ""); + solAssert(_type.isDynamicallySized(), ""); + + string functionName = "clean_up_bytearray_end_slots_" + _type.identifier(); + return m_functionCollector.createFunction(functionName, [&](vector& _args, vector&) { + _args = {"array", "len", "startIndex"}; + return Whiskers(R"( + if gt(len, 31) { + let dataArea := (array) + let deleteStart := add(dataArea, (startIndex)) + // If we are clearing array to be short byte array, we want to clear only data starting from array data area. + if lt(startIndex, 32) { deleteStart := dataArea } + (deleteStart, add(dataArea, (len))) + } + )") + ("dataLocation", arrayDataAreaFunction(_type)) + ("div32Ceil", divide32CeilFunction()) + ("clearStorageRange", clearStorageRangeFunction(*_type.baseType())) + .render(); + }); +} + string YulUtilFunctions::decreaseByteArraySizeFunction(ArrayType const& _type) { string functionName = "byte_array_decrease_size_" + _type.identifier(); @@ -1239,13 +1339,13 @@ string YulUtilFunctions::decreaseByteArraySizeFunction(ArrayType const& _type) switch lt(newLen, 32) case 0 { let arrayDataStart := (array) - let deleteStart := add(arrayDataStart, div(add(newLen, 31), 32)) + let deleteStart := add(arrayDataStart, (newLen)) // we have to partially clear last slot that is still used let offset := and(newLen, 0x1f) if offset { (sub(deleteStart, 1), offset) } - (deleteStart, add(arrayDataStart, div(add(oldLen, 31), 32))) + (deleteStart, add(arrayDataStart, (oldLen))) sstore(array, or(mul(2, newLen), 1)) } @@ -1254,7 +1354,7 @@ string YulUtilFunctions::decreaseByteArraySizeFunction(ArrayType const& _type) case 1 { let arrayDataStart := (array) // clear whole old array, as we are transforming to short bytes array - (add(arrayDataStart, 1), add(arrayDataStart, div(add(oldLen, 31), 32))) + (add(arrayDataStart, 1), add(arrayDataStart, (oldLen))) (array, newLen) } default { @@ -1267,6 +1367,7 @@ string YulUtilFunctions::decreaseByteArraySizeFunction(ArrayType const& _type) ("partialClearStorageSlot", partialClearStorageSlotFunction()) ("clearStorageRange", clearStorageRangeFunction(*_type.baseType())) ("transitLongToShort", byteArrayTransitLongToShortFunction(_type)) + ("div32Ceil", divide32CeilFunction()) ("encodeUsedSetLen", shortByteArrayEncodeUsedAreaSetLengthFunction()) .render(); }); @@ -1805,28 +1906,19 @@ string YulUtilFunctions::copyByteArrayToStorageFunction(ArrayType const& _fromTy let oldLen := (sload(slot)) + // potentially truncate data + (slot, oldLen, newLen) + let srcOffset := 0 srcOffset := 0x20 - // This is not needed in all branches. - let dstDataArea - if or(gt(oldLen, 31), gt(newLen, 31)) { - dstDataArea := (slot) - } - - if gt(oldLen, 31) { - // potentially truncate data - let deleteStart := add(dstDataArea, div(add(newLen, 31), 32)) - if lt(newLen, 32) { deleteStart := dstDataArea } - (deleteStart, add(dstDataArea, div(add(oldLen, 31), 32))) - } switch gt(newLen, 31) case 1 { let loopEnd := and(newLen, not(0x1f)) src := (src) - let dstPtr := dstDataArea + let dstPtr := (slot) let i := 0 for { } lt(i, loopEnd) { i := add(i, 0x20) } { sstore(dstPtr, (add(src, srcOffset))) @@ -1860,7 +1952,7 @@ string YulUtilFunctions::copyByteArrayToStorageFunction(ArrayType const& _fromTy templ("dstDataLocation", arrayDataAreaFunction(_toType)); if (fromStorage) templ("srcDataLocation", arrayDataAreaFunction(_fromType)); - templ("clearStorageRange", clearStorageRangeFunction(*_toType.baseType())); + templ("cleanUpEndArray", cleanUpDynamicByteArrayEndSlotsFunction(_toType)); templ("srcIncrement", to_string(fromStorage ? 1 : 0x20)); templ("read", fromStorage ? "sload" : fromCalldata ? "calldataload" : "mload"); templ("maskBytes", maskBytesFunctionDynamic()); @@ -2220,16 +2312,16 @@ string YulUtilFunctions::calldataArrayIndexRangeAccess(ArrayType const& _type) return m_functionCollector.createFunction(functionName, [&]() { return Whiskers(R"( function (offset, length, startIndex, endIndex) -> offsetOut, lengthOut { - if gt(startIndex, endIndex) { } - if gt(endIndex, length) { } + if gt(startIndex, endIndex) { () } + if gt(endIndex, length) { () } offsetOut := add(offset, mul(startIndex, )) lengthOut := sub(endIndex, startIndex) } )") ("functionName", functionName) ("stride", to_string(_type.calldataStride())) - ("revertSliceStartAfterEnd", revertReasonIfDebug("Slice starts after end")) - ("revertSliceGreaterThanLength", revertReasonIfDebug("Slice is greater than length")) + ("revertSliceStartAfterEnd", revertReasonIfDebugFunction("Slice starts after end")) + ("revertSliceGreaterThanLength", revertReasonIfDebugFunction("Slice is greater than length")) .render(); }); } @@ -2243,13 +2335,13 @@ string YulUtilFunctions::accessCalldataTailFunction(Type const& _type) return Whiskers(R"( function (base_ref, ptr_to_tail) -> addr, length { let rel_offset_of_tail := calldataload(ptr_to_tail) - if iszero(slt(rel_offset_of_tail, sub(sub(calldatasize(), base_ref), sub(, 1)))) { } + if iszero(slt(rel_offset_of_tail, sub(sub(calldatasize(), base_ref), sub(, 1)))) { () } addr := add(base_ref, rel_offset_of_tail) length := calldataload(addr) - if gt(length, 0xffffffffffffffff) { } + if gt(length, 0xffffffffffffffff) { () } addr := add(addr, 32) - if sgt(addr, sub(calldatasize(), mul(length, ))) { } + if sgt(addr, sub(calldatasize(), mul(length, ))) { () } } )") @@ -2257,9 +2349,9 @@ string YulUtilFunctions::accessCalldataTailFunction(Type const& _type) ("dynamicallySized", _type.isDynamicallySized()) ("neededLength", toCompactHexWithPrefix(_type.calldataEncodedTailSize())) ("calldataStride", toCompactHexWithPrefix(_type.isDynamicallySized() ? dynamic_cast(_type).calldataStride() : 0)) - ("invalidCalldataTailOffset", revertReasonIfDebug("Invalid calldata tail offset")) - ("invalidCalldataTailLength", revertReasonIfDebug("Invalid calldata tail length")) - ("shortCalldataTail", revertReasonIfDebug("Calldata tail too short")) + ("invalidCalldataTailOffset", revertReasonIfDebugFunction("Invalid calldata tail offset")) + ("invalidCalldataTailLength", revertReasonIfDebugFunction("Invalid calldata tail length")) + ("shortCalldataTail", revertReasonIfDebugFunction("Calldata tail too short")) .render(); }); } @@ -2381,11 +2473,12 @@ string YulUtilFunctions::bytesConcatFunction(vector const& _argumen targetTypes.emplace_back(argumentType); else if ( auto const* literalType = dynamic_cast(argumentType); - literalType && literalType->value().size() <= 32 + literalType && !literalType->value().empty() && literalType->value().size() <= 32 ) targetTypes.emplace_back(TypeProvider::fixedBytes(static_cast(literalType->value().size()))); else { + solAssert(!dynamic_cast(argumentType), ""); solAssert(argumentType->isImplicitlyConvertibleTo(*TypeProvider::bytesMemory()), ""); targetTypes.emplace_back(TypeProvider::bytesMemory()); } @@ -2649,15 +2742,13 @@ string YulUtilFunctions::updateStorageValueFunction( return Whiskers(R"( function (slot, offset) { if offset { () } - let value := () - (slot, value) + (slot) } )") ("functionName", functionName) ("dynamicOffset", !_offset.has_value()) ("panic", panicFunction(PanicCode::Generic)) - ("copyLiteralToMemory", copyLiteralToMemoryFunction(dynamic_cast(_fromType).value())) - ("copyToStorage", copyArrayToStorageFunction(*TypeProvider::bytesMemory(), toArrayType)) + ("copyToStorage", copyLiteralToStorageFunction(dynamic_cast(_fromType).value())) .render(); } @@ -2666,7 +2757,10 @@ string YulUtilFunctions::updateStorageValueFunction( fromReferenceType->isPointer() ).get() == *fromReferenceType, ""); - solAssert(toReferenceType->category() == fromReferenceType->category(), ""); + if (fromReferenceType->category() == Type::Category::ArraySlice) + solAssert(toReferenceType->category() == Type::Category::Array, ""); + else + solAssert(toReferenceType->category() == fromReferenceType->category(), ""); solAssert(_offset.value_or(0) == 0, ""); Whiskers templ(R"( @@ -2684,6 +2778,17 @@ string YulUtilFunctions::updateStorageValueFunction( dynamic_cast(_fromType), dynamic_cast(_toType) )); + else if (_fromType.category() == Type::Category::ArraySlice) + { + solAssert( + _fromType.dataStoredIn(DataLocation::CallData), + "Currently only calldata array slices are supported!" + ); + templ("copyToStorage", copyArrayToStorageFunction( + dynamic_cast(_fromType).arrayType(), + dynamic_cast(_toType) + )); + } else templ("copyToStorage", copyStructToStorageFunction( dynamic_cast(_fromType), @@ -3792,11 +3897,13 @@ string YulUtilFunctions::forwardingRevertFunction() if (forward) return Whiskers(R"( function () { - returndatacopy(0, 0, returndatasize()) - revert(0, returndatasize()) + let pos := () + returndatacopy(pos, 0, returndatasize()) + revert(pos, returndatasize()) } )") ("functionName", functionName) + ("allocateUnbounded", allocateUnboundedFunction()) .render(); else return Whiskers(R"( @@ -4210,42 +4317,52 @@ string YulUtilFunctions::readFromMemoryOrCalldata(Type const& _type, bool _fromC }); } -string YulUtilFunctions::revertReasonIfDebug(RevertStrings revertStrings, string const& _message) +string YulUtilFunctions::revertReasonIfDebugFunction(string const& _message) { - if (revertStrings >= RevertStrings::Debug && !_message.empty()) - { - Whiskers templ(R"({ - mstore(0, ) - mstore(4, 0x20) - mstore(add(4, 0x20), ) - let reasonPos := add(4, 0x40) - <#word> - mstore(add(reasonPos, ), ) - - revert(0, add(reasonPos, )) - })"); - templ("sig", util::selectorFromSignature("Error(string)").str()); - templ("length", to_string(_message.length())); - - size_t words = (_message.length() + 31) / 32; - vector> wordParams(words); - for (size_t i = 0; i < words; ++i) - { - wordParams[i]["offset"] = to_string(i * 32); - wordParams[i]["wordValue"] = formatAsStringOrNumber(_message.substr(32 * i, 32)); - } - templ("word", wordParams); - templ("end", to_string(words * 32)); - - return templ.render(); - } - else - return "revert(0, 0)"; + string functionName = "revert_error_" + util::toHex(util::keccak256(_message).asBytes()); + return m_functionCollector.createFunction(functionName, [&](auto&, auto&) -> string { + return revertReasonIfDebugBody(m_revertStrings, allocateUnboundedFunction() + "()", _message); + }); } -string YulUtilFunctions::revertReasonIfDebug(string const& _message) +string YulUtilFunctions::revertReasonIfDebugBody( + RevertStrings _revertStrings, + string const& _allocation, + string const& _message +) { - return revertReasonIfDebug(m_revertStrings, _message); + if (_revertStrings < RevertStrings::Debug || _message.empty()) + return "revert(0, 0)"; + + Whiskers templ(R"( + let start := + let pos := start + mstore(pos, ) + pos := add(pos, 4) + mstore(pos, 0x20) + pos := add(pos, 0x20) + mstore(pos, ) + pos := add(pos, 0x20) + <#word> + mstore(add(pos, ), ) + + revert(start, ) + )"); + templ("allocate", _allocation); + templ("sig", util::selectorFromSignature("Error(string)").str()); + templ("length", to_string(_message.length())); + + size_t words = (_message.length() + 31) / 32; + vector> wordParams(words); + for (size_t i = 0; i < words; ++i) + { + wordParams[i]["offset"] = to_string(i * 32); + wordParams[i]["wordValue"] = formatAsStringOrNumber(_message.substr(32 * i, 32)); + } + templ("word", wordParams); + templ("overallLength", to_string(4 + 0x20 + 0x20 + words * 32)); + + return templ.render(); } string YulUtilFunctions::panicFunction(util::PanicCode _code) @@ -4261,7 +4378,7 @@ string YulUtilFunctions::panicFunction(util::PanicCode _code) )") ("functionName", functionName) ("selector", util::selectorFromSignature("Panic(uint256)").str()) - ("code", toCompactHexWithPrefix(_code)) + ("code", toCompactHexWithPrefix(static_cast(_code))) .render(); }); } diff --git a/libsolidity/codegen/YulUtilFunctions.h b/libsolidity/codegen/YulUtilFunctions.h index f1eaed9fe..6b7aa1545 100644 --- a/libsolidity/codegen/YulUtilFunctions.h +++ b/libsolidity/codegen/YulUtilFunctions.h @@ -81,6 +81,10 @@ public: /// signature: (memPtr) -> std::string storeLiteralInMemoryFunction(std::string const& _literal); + /// @returns the name of a function that stores a string literal at a specific location in storage + /// signature: (slot) -> + std::string copyLiteralToStorageFunction(std::string const& _literal); + // @returns the name of a function that has the equivalent logic of an // `assert` or `require` call. std::string requireOrAssertFunction(bool _assert, Type const* _messageType = nullptr); @@ -126,9 +130,16 @@ public: /// @returns the name of a function that rounds its input to the next multiple /// of 32 or the input if it is a multiple of 32. + /// Ignores overflow. /// signature: (value) -> result std::string roundUpFunction(); + /// @returns the name of a function that divides by 32 and rounds up during the division. + /// In other words, on input x it returns the smallest y such that y * 32 >= x. + /// Ignores overflow. + /// signature: (x) -> y + std::string divide32CeilFunction(); + /// signature: (x, y) -> sum std::string overflowCheckedIntAddFunction(IntegerType const& _type); /// signature: (x, y) -> sum @@ -207,6 +218,11 @@ public: /// signature: (array, newLen) std::string resizeArrayFunction(ArrayType const& _type); + /// @returns the name of a function that zeroes all storage array elements from `startIndex` to `len` (excluding). + /// Assumes that `len` is the array length. Does nothing if `startIndex >= len`. Does not modify the stored length. + /// signature: (array, len, startIndex) + std::string cleanUpStorageArrayEndFunction(ArrayType const& _type); + /// @returns the name of a function that reduces the size of a storage array by one element /// signature: (array) std::string storageArrayPopFunction(ArrayType const& _type); @@ -453,12 +469,18 @@ public: /// signature: (slot, offset) -> std::string storageSetToZeroFunction(Type const& _type); - /// If revertStrings is debug, @returns inline assembly code that + /// If revertStrings is debug, @returns the name of a function that /// stores @param _message in memory position 0 and reverts. - /// Otherwise returns "revert(0, 0)". - static std::string revertReasonIfDebug(RevertStrings revertStrings, std::string const& _message = ""); + /// Otherwise returns the name of a function that uses "revert(0, 0)". + std::string revertReasonIfDebugFunction(std::string const& _message = ""); - std::string revertReasonIfDebug(std::string const& _message = ""); + /// @returns the function body of ``revertReasonIfDebug``. + /// Should only be used internally and by the old code generator. + static std::string revertReasonIfDebugBody( + RevertStrings _revertStrings, + std::string const& _allocation, + std::string const& _message + ); /// Reverts with ``Panic(uint256)`` and the given code. std::string panicFunction(util::PanicCode _code); @@ -537,6 +559,14 @@ private: /// signature: (array, newLen) std::string resizeDynamicByteArrayFunction(ArrayType const& _type); + /// @returns the name of a function that cleans up elements of a storage byte array starting from startIndex. + /// It will not copy elements in case of transformation to short byte array, and will not change array length. + /// In case of startIndex is greater than len, doesn't do anything. + /// In case of short byte array (< 32 bytes) doesn't do anything. + /// If the first slot to be cleaned up is partially occupied, does not touch it. Cleans up only completely unused slots. + /// signature: (array, len, startIndex) + std::string cleanUpDynamicByteArrayEndSlotsFunction(ArrayType const& _type); + /// @returns the name of a function that increases size of byte array /// when we resize byte array frextractUsedSetLenom < 32 elements to >= 32 elements or we push to byte array of size 31 copying of data will occur /// signature: (array, data, oldLen, newLen) diff --git a/libsolidity/codegen/ir/Common.cpp b/libsolidity/codegen/ir/Common.cpp index 3fbcfe746..4f414cb99 100644 --- a/libsolidity/codegen/ir/Common.cpp +++ b/libsolidity/codegen/ir/Common.cpp @@ -16,8 +16,9 @@ */ // SPDX-License-Identifier: GPL-3.0 -#include #include +#include +#include #include @@ -25,6 +26,9 @@ using namespace std; using namespace solidity::util; using namespace solidity::frontend; +namespace solidity::frontend +{ + YulArity YulArity::fromType(FunctionType const& _functionType) { return YulArity{ @@ -36,7 +40,7 @@ YulArity YulArity::fromType(FunctionType const& _functionType) string IRNames::function(FunctionDefinition const& _function) { if (_function.isConstructor()) - return implicitConstructor(*_function.annotation().contract); + return constructor(*_function.annotation().contract); return "fun_" + _function.name() + "_" + to_string(_function.id()); } @@ -78,7 +82,7 @@ string IRNames::internalDispatch(YulArity const& _arity) "_out_" + to_string(_arity.out); } -string IRNames::implicitConstructor(ContractDefinition const& _contract) +string IRNames::constructor(ContractDefinition const& _contract) { return "constructor_" + _contract.name() + "_" + to_string(_contract.id()); } @@ -122,3 +126,20 @@ string IRNames::zeroValue(Type const& _type, string const& _variableName) { return "zero_" + _type.identifier() + _variableName; } + +string sourceLocationComment(langutil::SourceLocation const& _location, IRGenerationContext const& _context) +{ + return "/// @src " + + to_string(_context.sourceIndices().at(_location.source->name())) + + ":" + + to_string(_location.start) + + "," + + to_string(_location.end); +} + +string sourceLocationComment(ASTNode const& _node, IRGenerationContext const& _context) +{ + return sourceLocationComment(_node.location(), _context); +} + +} diff --git a/libsolidity/codegen/ir/Common.h b/libsolidity/codegen/ir/Common.h index 23d74cd21..67b0cffdc 100644 --- a/libsolidity/codegen/ir/Common.h +++ b/libsolidity/codegen/ir/Common.h @@ -29,6 +29,8 @@ namespace solidity::frontend { +class IRGenerationContext; + /** * Structure that describes arity and co-arity of a Yul function, i.e. the number of its inputs and outputs. */ @@ -54,7 +56,7 @@ struct IRNames static std::string creationObject(ContractDefinition const& _contract); static std::string deployedObject(ContractDefinition const& _contract); static std::string internalDispatch(YulArity const& _arity); - static std::string implicitConstructor(ContractDefinition const& _contract); + static std::string constructor(ContractDefinition const& _contract); static std::string libraryAddressImmutable(); static std::string constantValueFunction(VariableDeclaration const& _constant); static std::string localVariable(VariableDeclaration const& _declaration); @@ -66,6 +68,14 @@ struct IRNames static std::string zeroValue(Type const& _type, std::string const& _variableName); }; + +/** + * @returns a source location comment in the form of + * `/// @src ::`. + */ +std::string sourceLocationComment(langutil::SourceLocation const& _location, IRGenerationContext const& _context); +std::string sourceLocationComment(ASTNode const& _node, IRGenerationContext const& _context); + } // Overloading std::less() makes it possible to use YulArity as a map key. We could define operator< diff --git a/libsolidity/codegen/ir/IRGenerationContext.cpp b/libsolidity/codegen/ir/IRGenerationContext.cpp index 228bef068..5d39bf4b5 100644 --- a/libsolidity/codegen/ir/IRGenerationContext.cpp +++ b/libsolidity/codegen/ir/IRGenerationContext.cpp @@ -178,7 +178,16 @@ ABIFunctions IRGenerationContext::abiFunctions() return ABIFunctions(m_evmVersion, m_revertStrings, m_functions); } -std::string IRGenerationContext::revertReasonIfDebug(std::string const& _message) +uint64_t IRGenerationContext::internalFunctionID(FunctionDefinition const& _function, bool _requirePresent) { - return YulUtilFunctions::revertReasonIfDebug(m_revertStrings, _message); + auto [iterator, inserted] = m_functionIDs.try_emplace(_function.id(), m_functionIDs.size() + 1); + if (_requirePresent) + solAssert(!inserted, ""); + return iterator->second; +} + +void IRGenerationContext::copyFunctionIDsFrom(IRGenerationContext const& _other) +{ + solAssert(m_functionIDs.empty(), ""); + m_functionIDs = _other.m_functionIDs; } diff --git a/libsolidity/codegen/ir/IRGenerationContext.h b/libsolidity/codegen/ir/IRGenerationContext.h index 9976367d9..1790655a2 100644 --- a/libsolidity/codegen/ir/IRGenerationContext.h +++ b/libsolidity/codegen/ir/IRGenerationContext.h @@ -68,11 +68,13 @@ public: IRGenerationContext( langutil::EVMVersion _evmVersion, RevertStrings _revertStrings, - OptimiserSettings _optimiserSettings + OptimiserSettings _optimiserSettings, + std::map _sourceIndices ): m_evmVersion(_evmVersion), m_revertStrings(_revertStrings), - m_optimiserSettings(std::move(_optimiserSettings)) + m_optimiserSettings(std::move(_optimiserSettings)), + m_sourceIndices(std::move(_sourceIndices)) {} MultiUseYulFunctionCollector& functionCollector() { return m_functions; } @@ -143,10 +145,6 @@ public: ABIFunctions abiFunctions(); - /// @returns code that stores @param _message for revert reason - /// if m_revertStrings is debug. - std::string revertReasonIfDebug(std::string const& _message = ""); - RevertStrings revertStrings() const { return m_revertStrings; } std::set& subObjectsCreated() { return m_subObjects; } @@ -154,10 +152,21 @@ public: bool inlineAssemblySeen() const { return m_inlineAssemblySeen; } void setInlineAssemblySeen() { m_inlineAssemblySeen = true; } + /// @returns the runtime ID to be used for the function in the dispatch routine + /// and for internal function pointers. + /// @param _requirePresent if false, generates a new ID if not yet done. + uint64_t internalFunctionID(FunctionDefinition const& _function, bool _requirePresent); + /// Copies the internal function IDs from the @a _other. For use in transferring + /// function IDs from constructor code to deployed code. + void copyFunctionIDsFrom(IRGenerationContext const& _other); + + std::map const& sourceIndices() const { return m_sourceIndices; } + private: langutil::EVMVersion m_evmVersion; RevertStrings m_revertStrings; OptimiserSettings m_optimiserSettings; + std::map m_sourceIndices; ContractDefinition const* m_mostDerivedContract = nullptr; std::map m_localVariables; /// Memory offsets reserved for the values of immutable variables during contract creation. @@ -190,6 +199,8 @@ private: /// the code contains a call via a pointer even though a specific function is never assigned to it. /// It will fail at runtime but the code must still compile. InternalDispatchMap m_internalDispatchMap; + /// Map used by @a internalFunctionID. + std::map m_functionIDs; std::set m_subObjects; }; diff --git a/libsolidity/codegen/ir/IRGenerator.cpp b/libsolidity/codegen/ir/IRGenerator.cpp index 4837b3ff2..9aab3ff5d 100644 --- a/libsolidity/codegen/ir/IRGenerator.cpp +++ b/libsolidity/codegen/ir/IRGenerator.cpp @@ -21,8 +21,8 @@ * Component that translates Solidity code into Yul. */ +#include #include - #include #include @@ -46,7 +46,6 @@ #include using namespace std; -using namespace ranges; using namespace solidity; using namespace solidity::util; using namespace solidity::frontend; @@ -80,7 +79,7 @@ set collectReachableCallables( ) { set reachableCallables; - for (CallGraph::Node const& reachableNode: _graph.edges | views::keys) + for (CallGraph::Node const& reachableNode: _graph.edges | ranges::views::keys) if (holds_alternative(reachableNode)) reachableCallables.emplace(get(reachableNode)); @@ -91,10 +90,11 @@ set collectReachableCallables( pair IRGenerator::run( ContractDefinition const& _contract, + bytes const& _cborMetadata, map const& _otherYulSources ) { - string const ir = yul::reindent(generate(_contract, _otherYulSources)); + string const ir = yul::reindent(generate(_contract, _cborMetadata, _otherYulSources)); yul::AssemblyStack asmStack(m_evmVersion, yul::AssemblyStack::Language::StrictAssembly, m_optimiserSettings); if (!asmStack.parseAndAnalyze("", ir)) @@ -107,18 +107,19 @@ pair IRGenerator::run( asmStack.optimize(); string warning = - "/*******************************************************\n" + "/*=====================================================*\n" " * WARNING *\n" " * Solidity to Yul compilation is still EXPERIMENTAL *\n" " * It can result in LOSS OF FUNDS or worse *\n" " * !USE AT YOUR OWN RISK! *\n" - " *******************************************************/\n\n"; + " *=====================================================*/\n\n"; return {warning + ir, warning + asmStack.print()}; } string IRGenerator::generate( ContractDefinition const& _contract, + bytes const& _cborMetadata, map const& _otherYulSources ) { @@ -133,18 +134,20 @@ string IRGenerator::generate( Whiskers t(R"( object "" { code { + let := () - () + () } object "" { code { + let called_via_delegatecall := iszero(eq(loadimmutable(""), address())) @@ -153,6 +156,7 @@ string IRGenerator::generate( } + data "" hex"" } } @@ -162,6 +166,8 @@ string IRGenerator::generate( for (VariableDeclaration const* var: ContractType(_contract).immutableVariables()) m_context.registerImmutableVariable(*var); + t("sourceLocationComment", sourceLocationComment(_contract, m_context)); + t("CreationObject", IRNames::creationObject(_contract)); t("library", _contract.isLibrary()); @@ -179,12 +185,12 @@ string IRGenerator::generate( } t("constructorParams", joinHumanReadable(constructorParams)); t("constructorHasParams", !constructorParams.empty()); - t("implicitConstructor", IRNames::implicitConstructor(_contract)); + t("constructor", IRNames::constructor(_contract)); t("deploy", deployCode(_contract)); - generateImplicitConstructors(_contract); + generateConstructors(_contract); set creationFunctionList = generateQueuedFunctions(); - InternalDispatchMap internalDispatchMap = generateInternalDispatchFunctions(); + InternalDispatchMap internalDispatchMap = generateInternalDispatchFunctions(_contract); t("functions", m_context.functionCollector().requestedFunctions()); t("subObjects", subObjectSources(m_context.subObjectsCreated())); @@ -205,9 +211,12 @@ string IRGenerator::generate( t("library_address", IRNames::libraryAddressImmutable()); t("dispatch", dispatchRoutine(_contract)); set deployedFunctionList = generateQueuedFunctions(); - generateInternalDispatchFunctions(); + generateInternalDispatchFunctions(_contract); t("deployedFunctions", m_context.functionCollector().requestedFunctions()); t("deployedSubObjects", subObjectSources(m_context.subObjectsCreated())); + t("metadataName", yul::Object::metadataName()); + t("cborMetadata", toHex(_cborMetadata)); + // This has to be called only after all other code generation for the deployed object is complete. bool deployedInvolvesAssembly = m_context.inlineAssemblySeen(); @@ -244,7 +253,7 @@ set IRGenerator::generateQueuedFunctions() return functions; } -InternalDispatchMap IRGenerator::generateInternalDispatchFunctions() +InternalDispatchMap IRGenerator::generateInternalDispatchFunctions(ContractDefinition const& _contract) { solAssert( m_context.functionGenerationQueueEmpty(), @@ -259,6 +268,7 @@ InternalDispatchMap IRGenerator::generateInternalDispatchFunctions() m_context.functionCollector().createFunction(funName, [&]() { Whiskers templ(R"( function (fun, ) -> { + switch fun <#cases> case @@ -269,6 +279,7 @@ InternalDispatchMap IRGenerator::generateInternalDispatchFunctions() default { () } } )"); + templ("sourceLocationComment", sourceLocationComment(_contract, m_context)); templ("functionName", funName); templ("panic", m_utils.panicFunction(PanicCode::InvalidInternalFunction)); templ("in", suffixedVariableNameList("in_", 0, arity.in)); @@ -288,7 +299,7 @@ InternalDispatchMap IRGenerator::generateInternalDispatchFunctions() solAssert(m_context.functionCollector().contains(IRNames::function(*function)), ""); cases.emplace_back(map{ - {"funID", to_string(function->id())}, + {"funID", to_string(m_context.internalFunctionID(*function, true))}, {"name", IRNames::function(*function)} }); } @@ -314,10 +325,14 @@ string IRGenerator::generateFunction(FunctionDefinition const& _function) m_context.resetLocalVariables(); Whiskers t(R"( function () -> { + } )"); + + t("sourceLocationComment", sourceLocationComment(_function, m_context)); + t("functionName", functionName); vector params; for (auto const& varDecl: _function.parameters()) @@ -372,6 +387,7 @@ string IRGenerator::generateModifier( m_context.resetLocalVariables(); Whiskers t(R"( function () -> { + @@ -399,6 +415,7 @@ string IRGenerator::generateModifier( _modifierInvocation.name().annotation().referencedDeclaration ); solAssert(modifier, ""); + t("sourceLocationComment", sourceLocationComment(*modifier, m_context)); switch (*_modifierInvocation.name().annotation().requiredLookup) { case VirtualLookup::Virtual: @@ -450,10 +467,12 @@ string IRGenerator::generateFunctionWithModifierInner(FunctionDefinition const& m_context.resetLocalVariables(); Whiskers t(R"( function () -> { + } )"); + t("sourceLocationComment", sourceLocationComment(_function, m_context)); t("functionName", functionName); vector retParams; vector retParamsIn; @@ -492,9 +511,11 @@ string IRGenerator::generateGetter(VariableDeclaration const& _varDecl) solUnimplementedAssert(type->sizeOnStack() == 1, ""); return Whiskers(R"( function () -> rval { + rval := loadimmutable("") } )") + ("sourceLocationComment", sourceLocationComment(_varDecl, m_context)) ("functionName", functionName) ("id", to_string(_varDecl.id())) .render(); @@ -504,9 +525,11 @@ string IRGenerator::generateGetter(VariableDeclaration const& _varDecl) solAssert(paramTypes.empty(), ""); return Whiskers(R"( function () -> { + := () } )") + ("sourceLocationComment", sourceLocationComment(_varDecl, m_context)) ("functionName", functionName) ("constantValueFunction", IRGeneratorForStatements(m_context, m_utils).constantValueFunction(_varDecl)) ("ret", suffixedVariableNameList("ret_", 0, _varDecl.type()->sizeOnStack())) @@ -619,6 +642,7 @@ string IRGenerator::generateGetter(VariableDeclaration const& _varDecl) return Whiskers(R"( function () -> { + } )") @@ -626,6 +650,7 @@ string IRGenerator::generateGetter(VariableDeclaration const& _varDecl) ("params", joinHumanReadable(parameters)) ("retVariables", joinHumanReadable(returnVariables)) ("code", std::move(code)) + ("sourceLocationComment", sourceLocationComment(_varDecl, m_context)) .render(); }); } @@ -657,7 +682,6 @@ pair>> IRGenerator::evalua map>const *, InheritanceOrder> baseConstructorArguments(inheritanceOrder); - ; for (ASTPointer const& base: _contract.baseContracts()) if (FunctionDefinition const* baseConstructor = dynamic_cast( @@ -712,17 +736,17 @@ string IRGenerator::initStateVariables(ContractDefinition const& _contract) } -void IRGenerator::generateImplicitConstructors(ContractDefinition const& _contract) +void IRGenerator::generateConstructors(ContractDefinition const& _contract) { - auto listAllParams = [&]( - map> const& baseParams) -> vector - { - vector params; - for (ContractDefinition const* contract: _contract.annotation().linearizedBaseContracts) - if (baseParams.count(contract)) - params += baseParams.at(contract); - return params; - }; + auto listAllParams = + [&](map> const& baseParams) -> vector + { + vector params; + for (ContractDefinition const* contract: _contract.annotation().linearizedBaseContracts) + if (baseParams.count(contract)) + params += baseParams.at(contract); + return params; + }; map> baseConstructorParams; for (size_t i = 0; i < _contract.annotation().linearizedBaseContracts.size(); ++i) @@ -731,10 +755,11 @@ void IRGenerator::generateImplicitConstructors(ContractDefinition const& _contra baseConstructorParams.erase(contract); m_context.resetLocalVariables(); - m_context.functionCollector().createFunction(IRNames::implicitConstructor(*contract), [&]() { + m_context.functionCollector().createFunction(IRNames::constructor(*contract), [&]() { Whiskers t(R"( function () { + () @@ -744,11 +769,19 @@ void IRGenerator::generateImplicitConstructors(ContractDefinition const& _contra if (contract->constructor()) for (ASTPointer const& varDecl: contract->constructor()->parameters()) params += m_context.addLocalVariable(*varDecl).stackSlots(); + + t("sourceLocationComment", sourceLocationComment( + contract->constructor() ? + contract->constructor()->location() : + contract->location(), + m_context + )); + t("params", joinHumanReadable(params)); vector baseParams = listAllParams(baseConstructorParams); t("baseParams", joinHumanReadable(baseParams)); t("comma", !params.empty() && !baseParams.empty() ? ", " : ""); - t("functionName", IRNames::implicitConstructor(*contract)); + t("functionName", IRNames::constructor(*contract)); pair>> evaluatedArgs = evaluateConstructorArguments(*contract); baseConstructorParams.insert(evaluatedArgs.second.begin(), evaluatedArgs.second.end()); t("evalBaseArguments", evaluatedArgs.first); @@ -756,7 +789,7 @@ void IRGenerator::generateImplicitConstructors(ContractDefinition const& _contra { t("hasNextConstructor", true); ContractDefinition const* nextContract = _contract.annotation().linearizedBaseContracts[i + 1]; - t("nextConstructor", IRNames::implicitConstructor(*nextContract)); + t("nextConstructor", IRNames::constructor(*nextContract)); t("nextParams", joinHumanReadable(listAllParams(baseConstructorParams))); } else @@ -802,29 +835,24 @@ void IRGenerator::generateImplicitConstructors(ContractDefinition const& _contra string IRGenerator::deployCode(ContractDefinition const& _contract) { Whiskers t(R"X( - <#loadImmutables> - let := mload() - - - codecopy(0, dataoffset(""), datasize("")) - - <#storeImmutables> - setimmutable(0, "", ) - - - return(0, datasize("")) + let := () + codecopy(, dataoffset(""), datasize("")) + <#immutables> + setimmutable(, "", ) + + return(, datasize("")) )X"); + t("allocateUnbounded", m_utils.allocateUnboundedFunction()); + t("codeOffset", m_context.newYulVariable()); t("object", IRNames::deployedObject(_contract)); - vector> loadImmutables; - vector> storeImmutables; - + vector> immutables; if (_contract.isLibrary()) { solAssert(ContractType(_contract).immutableVariables().empty(), ""); - storeImmutables.emplace_back(map{ - {"var"s, "address()"}, - {"immutableName"s, IRNames::libraryAddressImmutable()} + immutables.emplace_back(map{ + {"immutableName"s, IRNames::libraryAddressImmutable()}, + {"value"s, "address()"} }); } @@ -833,26 +861,18 @@ string IRGenerator::deployCode(ContractDefinition const& _contract) { solUnimplementedAssert(immutable->type()->isValueType(), ""); solUnimplementedAssert(immutable->type()->sizeOnStack() == 1, ""); - string yulVar = m_context.newYulVariable(); - loadImmutables.emplace_back(map{ - {"var"s, yulVar}, - {"memoryOffset"s, to_string(m_context.immutableMemoryOffset(*immutable))} - }); - storeImmutables.emplace_back(map{ - {"var"s, yulVar}, - {"immutableName"s, to_string(immutable->id())} + immutables.emplace_back(map{ + {"immutableName"s, to_string(immutable->id())}, + {"value"s, "mload(" + to_string(m_context.immutableMemoryOffset(*immutable)) + ")"} }); } - t("loadImmutables", std::move(loadImmutables)); - // reverse order to ease stack strain - reverse(storeImmutables.begin(), storeImmutables.end()); - t("storeImmutables", std::move(storeImmutables)); + t("immutables", std::move(immutables)); return t.render(); } string IRGenerator::callValueCheck() { - return "if callvalue() { " + m_context.revertReasonIfDebug("Ether sent to non-payable function") + " }"; + return "if callvalue() { " + m_utils.revertReasonIfDebugFunction("Ether sent to non-payable function") + "() }"; } string IRGenerator::dispatchRoutine(ContractDefinition const& _contract) @@ -898,8 +918,8 @@ string IRGenerator::dispatchRoutine(ContractDefinition const& _contract) // we revert. delegatecallCheck = "if iszero(called_via_delegatecall) { " + - m_context.revertReasonIfDebug("Non-view function of library called without DELEGATECALL") + - " }"; + m_utils.revertReasonIfDebugFunction("Non-view function of library called without DELEGATECALL") + + "() }"; } templ["delegatecallCheck"] = delegatecallCheck; templ["callValueCheck"] = (type->isPayable() || _contract.isLibrary()) ? "" : callValueCheck(); @@ -950,12 +970,11 @@ string IRGenerator::dispatchRoutine(ContractDefinition const& _contract) t("fallback", fallbackCode); } else - t( - "fallback", + t("fallback", ( etherReceiver ? - m_context.revertReasonIfDebug("Unknown signature and no fallback defined") : - m_context.revertReasonIfDebug("Contract does not have fallback nor receive functions") - ); + m_utils.revertReasonIfDebugFunction("Unknown signature and no fallback defined") : + m_utils.revertReasonIfDebugFunction("Contract does not have fallback nor receive functions") + ) + "()"); return t.render(); } @@ -994,7 +1013,9 @@ void IRGenerator::resetContext(ContractDefinition const& _contract) m_context.internalDispatchClean(), "Reset internal dispatch map without consuming it." ); - m_context = IRGenerationContext(m_evmVersion, m_context.revertStrings(), m_optimiserSettings); + IRGenerationContext newContext(m_evmVersion, m_context.revertStrings(), m_optimiserSettings, m_context.sourceIndices()); + newContext.copyFunctionIDsFrom(m_context); + m_context = move(newContext); m_context.setMostDerivedContract(_contract); for (auto const& var: ContractType(_contract).stateVariables()) diff --git a/libsolidity/codegen/ir/IRGenerator.h b/libsolidity/codegen/ir/IRGenerator.h index 83621f206..528fc3c7b 100644 --- a/libsolidity/codegen/ir/IRGenerator.h +++ b/libsolidity/codegen/ir/IRGenerator.h @@ -42,11 +42,12 @@ public: IRGenerator( langutil::EVMVersion _evmVersion, RevertStrings _revertStrings, - OptimiserSettings _optimiserSettings + OptimiserSettings _optimiserSettings, + std::map _sourceIndices ): m_evmVersion(_evmVersion), m_optimiserSettings(_optimiserSettings), - m_context(_evmVersion, _revertStrings, std::move(_optimiserSettings)), + m_context(_evmVersion, _revertStrings, std::move(_optimiserSettings), std::move(_sourceIndices)), m_utils(_evmVersion, m_context.revertStrings(), m_context.functionCollector()) {} @@ -54,12 +55,14 @@ public: /// (or just pretty-printed, depending on the optimizer settings). std::pair run( ContractDefinition const& _contract, + bytes const& _cborMetadata, std::map const& _otherYulSources ); private: std::string generate( ContractDefinition const& _contract, + bytes const& _cborMetadata, std::map const& _otherYulSources ); std::string generate(Block const& _block); @@ -72,7 +75,7 @@ private: /// possibly be called via a pointer. /// @return The content of the dispatch for reuse in runtime code. Reuse is necessary because /// pointers to functions can be passed from the creation code in storage variables. - InternalDispatchMap generateInternalDispatchFunctions(); + InternalDispatchMap generateInternalDispatchFunctions(ContractDefinition const& _contract); /// Generates code for and returns the name of the function. std::string generateFunction(FunctionDefinition const& _function); std::string generateModifier( @@ -87,10 +90,10 @@ private: /// Generates code that assigns the initial value of the respective type. std::string generateInitialAssignment(VariableDeclaration const& _varDecl); - /// Generates implicit constructors for all contracts in the inheritance hierarchy of + /// Generates constructors for all contracts in the inheritance hierarchy of /// @a _contract - /// If there are user defined constructors, their body will be included in implicit constructors body. - void generateImplicitConstructors(ContractDefinition const& _contract); + /// If there are user defined constructors, their body will be included in the implicit constructor's body. + void generateConstructors(ContractDefinition const& _contract); /// Evaluates constructor's arguments for all base contracts (listed in inheritance specifiers) of /// @a _contract diff --git a/libsolidity/codegen/ir/IRGeneratorForStatements.cpp b/libsolidity/codegen/ir/IRGeneratorForStatements.cpp index e59fde079..aea70536a 100644 --- a/libsolidity/codegen/ir/IRGeneratorForStatements.cpp +++ b/libsolidity/codegen/ir/IRGeneratorForStatements.cpp @@ -192,9 +192,9 @@ private: solAssert(false, ""); if (isdigit(value.front())) - return yul::Literal{_identifier.location, yul::LiteralKind::Number, yul::YulString{value}, {}}; + return yul::Literal{_identifier.debugData, yul::LiteralKind::Number, yul::YulString{value}, {}}; else - return yul::Identifier{_identifier.location, yul::YulString{value}}; + return yul::Identifier{_identifier.debugData, yul::YulString{value}}; } @@ -205,10 +205,34 @@ private: } +string IRGeneratorForStatementsBase::code() const +{ + return m_code.str(); +} + +std::ostringstream& IRGeneratorForStatementsBase::appendCode(bool _addLocationComment) +{ + if ( + _addLocationComment && + m_currentLocation.isValid() && + m_lastLocation != m_currentLocation + ) + m_code << sourceLocationComment(m_currentLocation, m_context) << "\n"; + + m_lastLocation = m_currentLocation; + + return m_code; +} + +void IRGeneratorForStatementsBase::setLocation(ASTNode const& _node) +{ + m_currentLocation = _node.location(); +} + string IRGeneratorForStatements::code() const { solAssert(!m_currentLValue, "LValue not reset!"); - return m_code.str(); + return IRGeneratorForStatementsBase::code(); } void IRGeneratorForStatements::generate(Block const& _block) @@ -238,9 +262,6 @@ void IRGeneratorForStatements::initializeStateVar(VariableDeclaration const& _va _varDecl.value()->accept(*this); - Type const* rightIntermediateType = _varDecl.value()->annotation().type->closestTemporaryType(_varDecl.type()); - solAssert(rightIntermediateType, ""); - IRVariable value = convert(*_varDecl.value(), *rightIntermediateType); writeToLValue( _varDecl.immutable() ? IRLValue{*_varDecl.annotation().type, IRLValue::Immutable{&_varDecl}} : @@ -248,7 +269,7 @@ void IRGeneratorForStatements::initializeStateVar(VariableDeclaration const& _va util::toCompactHexWithPrefix(m_context.storageLocationOfStateVariable(_varDecl).first), m_context.storageLocationOfStateVariable(_varDecl).second }}, - value + *_varDecl.value() ); } catch (langutil::UnimplementedFeatureError const& _error) @@ -292,6 +313,8 @@ IRVariable IRGeneratorForStatements::evaluateExpression(Expression const& _expre setLocation(_expression); _expression.accept(*this); + + setLocation(_expression); IRVariable variable{m_context.newYulVariable(), _targetType}; define(variable, _expression); return variable; @@ -308,16 +331,16 @@ string IRGeneratorForStatements::constantValueFunction(VariableDeclaration const { try { - setLocation(_constant); - string functionName = IRNames::constantValueFunction(_constant); return m_context.functionCollector().createFunction(functionName, [&] { Whiskers templ(R"( + function () -> { := } )"); + templ("sourceLocationComment", sourceLocationComment(_constant, m_context)); templ("functionName", functionName); IRGeneratorForStatements generator(m_context, m_utils); solAssert(_constant.value(), ""); @@ -379,19 +402,19 @@ bool IRGeneratorForStatements::visit(Conditional const& _conditional) string condition = expressionAsType(_conditional.condition(), *TypeProvider::boolean()); declare(_conditional); - m_code << "switch " << condition << "\n" "case 0 {\n"; + appendCode() << "switch " << condition << "\n" "case 0 {\n"; _conditional.falseExpression().accept(*this); setLocation(_conditional); assign(_conditional, _conditional.falseExpression()); - m_code << "}\n" "default {\n"; + appendCode() << "}\n" "default {\n"; _conditional.trueExpression().accept(*this); setLocation(_conditional); assign(_conditional, _conditional.trueExpression()); - m_code << "}\n"; + appendCode() << "}\n"; return false; } @@ -407,55 +430,49 @@ bool IRGeneratorForStatements::visit(Assignment const& _assignment) assignmentOperator : TokenTraits::AssignmentToBinaryOp(assignmentOperator); - Type const* rightIntermediateType = - TokenTraits::isShiftOp(binaryOperator) ? - type(_assignment.rightHandSide()).mobileType() : - type(_assignment.rightHandSide()).closestTemporaryType( - &type(_assignment.leftHandSide()) - ); - solAssert(rightIntermediateType, ""); - IRVariable value = convert(_assignment.rightHandSide(), *rightIntermediateType); + if (TokenTraits::isShiftOp(binaryOperator)) + solAssert(type(_assignment.rightHandSide()).mobileType(), ""); + IRVariable value = + type(_assignment.leftHandSide()).isValueType() ? + convert( + _assignment.rightHandSide(), + TokenTraits::isShiftOp(binaryOperator) ? *type(_assignment.rightHandSide()).mobileType() : type(_assignment) + ) : + _assignment.rightHandSide(); + _assignment.leftHandSide().accept(*this); + solAssert(!!m_currentLValue, "LValue not retrieved."); setLocation(_assignment); if (assignmentOperator != Token::Assign) { solAssert(type(_assignment.leftHandSide()).isValueType(), "Compound operators only available for value types."); - solAssert(rightIntermediateType->isValueType(), "Compound operators only available for value types."); - IRVariable leftIntermediate = readFromLValue(*m_currentLValue); solAssert(binaryOperator != Token::Exp, ""); - if (TokenTraits::isShiftOp(binaryOperator)) - { - solAssert(type(_assignment) == leftIntermediate.type(), ""); - solAssert(type(_assignment) == type(_assignment.leftHandSide()), ""); - define(_assignment) << shiftOperation(binaryOperator, leftIntermediate, value) << "\n"; + solAssert(type(_assignment) == type(_assignment.leftHandSide()), ""); - writeToLValue(*m_currentLValue, IRVariable(_assignment)); - m_currentLValue.reset(); - return false; - } - else - { - solAssert(type(_assignment.leftHandSide()) == *rightIntermediateType, ""); - m_code << value.name() << " := " << binaryOperation( - binaryOperator, - *rightIntermediateType, - leftIntermediate.name(), - value.name() - ); - } + IRVariable leftIntermediate = readFromLValue(*m_currentLValue); + solAssert(type(_assignment) == leftIntermediate.type(), ""); + + define(_assignment) << ( + TokenTraits::isShiftOp(binaryOperator) ? + shiftOperation(binaryOperator, leftIntermediate, value) : + binaryOperation(binaryOperator, type(_assignment), leftIntermediate.name(), value.name()) + ) << "\n"; + + writeToLValue(*m_currentLValue, IRVariable(_assignment)); + } + else + { + writeToLValue(*m_currentLValue, value); + + if (dynamic_cast(&m_currentLValue->type)) + define(_assignment, readFromLValue(*m_currentLValue)); + else if (*_assignment.annotation().type != *TypeProvider::emptyTuple()) + define(_assignment, value); } - writeToLValue(*m_currentLValue, value); - - if (dynamic_cast(&m_currentLValue->type)) - define(_assignment, readFromLValue(*m_currentLValue)); - else if (*_assignment.annotation().type != *TypeProvider::emptyTuple()) - define(_assignment, value); - m_currentLValue.reset(); - return false; } @@ -481,7 +498,7 @@ bool IRGeneratorForStatements::visit(TupleExpression const& _tuple) component.accept(*this); setLocation(_tuple); IRVariable converted = convert(component, baseType); - m_code << + appendCode() << m_utils.writeToMemoryFunction(baseType) << "(" << ("add(" + mpos + ", " + to_string(i * arrayType.memoryStride()) + ")") << @@ -562,24 +579,25 @@ bool IRGeneratorForStatements::visit(IfStatement const& _ifStatement) if (_ifStatement.falseStatement()) { - m_code << "switch " << condition << "\n" "case 0 {\n"; + appendCode() << "switch " << condition << "\n" "case 0 {\n"; _ifStatement.falseStatement()->accept(*this); setLocation(_ifStatement); - m_code << "}\n" "default {\n"; + appendCode() << "}\n" "default {\n"; } else - m_code << "if " << condition << " {\n"; + appendCode() << "if " << condition << " {\n"; _ifStatement.trueStatement().accept(*this); setLocation(_ifStatement); - m_code << "}\n"; + appendCode() << "}\n"; return false; } -void IRGeneratorForStatements::endVisit(PlaceholderStatement const&) +void IRGeneratorForStatements::endVisit(PlaceholderStatement const& _placeholder) { solAssert(m_placeholderCallback, ""); - m_code << m_placeholderCallback(); + setLocation(_placeholder); + appendCode() << m_placeholderCallback(); } bool IRGeneratorForStatements::visit(ForStatement const& _forStatement) @@ -612,14 +630,14 @@ bool IRGeneratorForStatements::visit(WhileStatement const& _whileStatement) bool IRGeneratorForStatements::visit(Continue const& _continue) { setLocation(_continue); - m_code << "continue\n"; + appendCode() << "continue\n"; return false; } bool IRGeneratorForStatements::visit(Break const& _break) { setLocation(_break); - m_code << "break\n"; + appendCode() << "break\n"; return false; } @@ -637,22 +655,31 @@ void IRGeneratorForStatements::endVisit(Return const& _return) else if (returnParameters.size() == 1) assign(m_context.localVariable(*returnParameters.front()), *value); } - m_code << "leave\n"; + appendCode() << "leave\n"; } -void IRGeneratorForStatements::endVisit(UnaryOperation const& _unaryOperation) +bool IRGeneratorForStatements::visit(UnaryOperation const& _unaryOperation) { setLocation(_unaryOperation); Type const& resultType = type(_unaryOperation); Token const op = _unaryOperation.getOperator(); + if (resultType.category() == Type::Category::RationalNumber) + { + define(_unaryOperation) << formatNumber(resultType.literalValue(nullptr)) << "\n"; + return false; + } + + _unaryOperation.subExpression().accept(*this); + setLocation(_unaryOperation); + if (op == Token::Delete) { solAssert(!!m_currentLValue, "LValue not retrieved."); std::visit( util::GenericVisitor{ [&](IRLValue::Storage const& _storage) { - m_code << + appendCode() << m_utils.storageSetToZeroFunction(m_currentLValue->type) << "(" << _storage.slot << @@ -671,8 +698,6 @@ void IRGeneratorForStatements::endVisit(UnaryOperation const& _unaryOperation) m_currentLValue->kind ); } - else if (resultType.category() == Type::Category::RationalNumber) - define(_unaryOperation) << formatNumber(resultType.literalValue(nullptr)) << "\n"; else if (resultType.category() == Type::Category::Integer) { solAssert(resultType == type(_unaryOperation.subExpression()), "Result type doesn't match!"); @@ -731,6 +756,8 @@ void IRGeneratorForStatements::endVisit(UnaryOperation const& _unaryOperation) } else solUnimplementedAssert(false, "Unary operator not yet implemented"); + + return false; } bool IRGeneratorForStatements::visit(BinaryOperation const& _binOp) @@ -879,7 +906,7 @@ void IRGeneratorForStatements::endVisit(FunctionCall const& _functionCall) for (size_t i = 0; i < arguments.size(); i++) { IRVariable converted = convert(*arguments[i], *parameterTypes[i]); - m_code << + appendCode() << m_utils.writeToMemoryFunction(*functionType->parameterTypes()[i]) << "(add(" << IRVariable(_functionCall).part("mpos").name() << @@ -893,8 +920,6 @@ void IRGeneratorForStatements::endVisit(FunctionCall const& _functionCall) return; } - auto const* memberAccess = dynamic_cast(&_functionCall.expression()); - switch (functionType->kind()) { case FunctionType::Kind::Declaration: @@ -902,39 +927,7 @@ void IRGeneratorForStatements::endVisit(FunctionCall const& _functionCall) break; case FunctionType::Kind::Internal: { - auto identifier = dynamic_cast(&_functionCall.expression()); - auto const* functionDef = dynamic_cast( - ASTNode::referencedDeclaration(_functionCall.expression()) - ); - - if (functionDef) - { - solAssert(memberAccess || identifier, ""); - solAssert(functionType->declaration() == *functionDef, ""); - - if (identifier) - { - solAssert(*identifier->annotation().requiredLookup == VirtualLookup::Virtual, ""); - functionDef = &functionDef->resolveVirtual(m_context.mostDerivedContract()); - } - else if (auto typeType = dynamic_cast(memberAccess->expression().annotation().type)) - if ( - auto contractType = dynamic_cast(typeType->actualType()); - contractType->isSuper() - ) - { - ContractDefinition const* super = contractType->contractDefinition().superContract(m_context.mostDerivedContract()); - solAssert(super, "Super contract not available."); - solAssert(*memberAccess->annotation().requiredLookup == VirtualLookup::Super, ""); - functionDef = &functionDef->resolveVirtual(m_context.mostDerivedContract(), super); - } - - solAssert(functionDef && functionDef->isImplemented(), ""); - solAssert( - functionDef->parameters().size() == arguments.size() + (functionType->bound() ? 1 : 0), - "" - ); - } + FunctionDefinition const* functionDef = ASTNode::resolveFunctionCall(_functionCall, &m_context.mostDerivedContract()); solAssert(!functionType->takesArbitraryParameters(), ""); @@ -946,11 +939,15 @@ void IRGeneratorForStatements::endVisit(FunctionCall const& _functionCall) args += convert(*arguments[i], *parameterTypes[i]).stackSlots(); if (functionDef) + { + solAssert(functionDef->isImplemented(), ""); + define(_functionCall) << m_context.enqueueFunctionForCodeGeneration(*functionDef) << "(" << joinHumanReadable(args) << ")\n"; + } else { YulArity arity = YulArity::fromType(*functionType); @@ -1040,7 +1037,7 @@ void IRGeneratorForStatements::endVisit(FunctionCall const& _functionCall) templ("indexedArgs", joinHumanReadablePrefixed(indexedArgs | ranges::views::transform([&](auto const& _arg) { return _arg.commaSeparatedList(); }))); - m_code << templ.render(); + appendCode() << templ.render(); break; } case FunctionType::Kind::Error: @@ -1069,10 +1066,10 @@ void IRGeneratorForStatements::endVisit(FunctionCall const& _functionCall) messageArgumentType ); - m_code << move(requireOrAssertFunction) << "(" << IRVariable(*arguments[0]).name(); + appendCode() << move(requireOrAssertFunction) << "(" << IRVariable(*arguments[0]).name(); if (messageArgumentType && messageArgumentType->sizeOnStack() > 0) - m_code << ", " << IRVariable(*arguments[1]).commaSeparatedList(); - m_code << ")\n"; + appendCode() << ", " << IRVariable(*arguments[1]).commaSeparatedList(); + appendCode() << ")\n"; break; } @@ -1114,7 +1111,7 @@ void IRGeneratorForStatements::endVisit(FunctionCall const& _functionCall) // We might want to introduce a new set of memory handling functions here // a la "setMemoryCheckPoint" and "freeUntilCheckPoint". string freeMemoryPre = m_context.newYulVariable(); - m_code << "let " << freeMemoryPre << " := " << m_utils.allocateUnboundedFunction() << "()\n"; + appendCode() << "let " << freeMemoryPre << " := " << m_utils.allocateUnboundedFunction() << "()\n"; IRVariable array = convert(*arguments[0], *TypeProvider::bytesMemory()); IRVariable hashVariable(m_context.newYulVariable(), *TypeProvider::fixedBytes(32)); @@ -1131,7 +1128,7 @@ void IRGeneratorForStatements::endVisit(FunctionCall const& _functionCall) IRVariable selectorVariable(m_context.newYulVariable(), *TypeProvider::fixedBytes(4)); define(selectorVariable, hashVariable); selector = selectorVariable.name(); - m_code << m_utils.finalizeAllocationFunction() << "(" << freeMemoryPre << ", 0)\n"; + appendCode() << m_utils.finalizeAllocationFunction() << "(" << freeMemoryPre << ", 0)\n"; } } else if (functionType->kind() == FunctionType::Kind::ABIEncodeWithSelector) @@ -1161,7 +1158,7 @@ void IRGeneratorForStatements::endVisit(FunctionCall const& _functionCall) templ("arguments", joinHumanReadablePrefixed(argumentVars)); templ("finalizeAllocation", m_utils.finalizeAllocationFunction()); - m_code << templ.render(); + appendCode() << templ.render(); break; } case FunctionType::Kind::ABIDecode: @@ -1200,7 +1197,7 @@ void IRGeneratorForStatements::endVisit(FunctionCall const& _functionCall) } templ("retVars", IRVariable(_functionCall).commaSeparatedList()); - m_code << templ.render(); + appendCode() << templ.render(); break; } case FunctionType::Kind::Revert: @@ -1212,7 +1209,7 @@ void IRGeneratorForStatements::endVisit(FunctionCall const& _functionCall) arguments.front()->annotation().type->isImplicitlyConvertibleTo(*TypeProvider::stringMemory()), ""); if (m_context.revertStrings() == RevertStrings::Strip || arguments.empty()) - m_code << "revert(0, 0)\n"; + appendCode() << "revert(0, 0)\n"; else revertWithError( "Error(string)", @@ -1287,7 +1284,7 @@ void IRGeneratorForStatements::endVisit(FunctionCall const& _functionCall) { auto slotName = m_context.newYulVariable(); auto offsetName = m_context.newYulVariable(); - m_code << "let " << slotName << ", " << offsetName << " := " << + appendCode() << "let " << slotName << ", " << offsetName << " := " << m_utils.storageArrayPushZeroFunction(*arrayType) << "(" << IRVariable(_functionCall.expression()).commaSeparatedList() << ")\n"; setLValue(_functionCall, IRLValue{ @@ -1305,7 +1302,7 @@ void IRGeneratorForStatements::endVisit(FunctionCall const& _functionCall) convert(*arguments.front(), *arrayType->baseType()) : *arguments.front(); - m_code << + appendCode() << m_utils.storageArrayPushFunction(*arrayType, &argument.type()) << "(" << IRVariable(_functionCall.expression()).commaSeparatedList() << @@ -1350,7 +1347,7 @@ void IRGeneratorForStatements::endVisit(FunctionCall const& _functionCall) Whiskers templ("if iszero() { () }\n"); templ("modulus", modulus.name()); templ("panic", m_utils.panicFunction(PanicCode::DivisionByZero)); - m_code << templ.render(); + appendCode() << templ.render(); string args; for (size_t i = 0; i < 2; ++i) @@ -1396,8 +1393,7 @@ void IRGeneratorForStatements::endVisit(FunctionCall const& _functionCall) &dynamic_cast(*functionType->returnParameterTypes().front()).contractDefinition(); m_context.subObjectsCreated().insert(contract); - Whiskers t(R"( - let := () + Whiskers t(R"(let := () let := add(, datasize("")) if or(gt(, 0xffffffffffffffff), lt(, )) { () } datacopy(, dataoffset(""), datasize("")) @@ -1433,7 +1429,7 @@ void IRGeneratorForStatements::endVisit(FunctionCall const& _functionCall) t("success", IRNames::trySuccessConditionVariable(_functionCall)); else t("forwardingRevert", m_utils.forwardingRevertFunction()); - m_code << t.render(); + appendCode() << t.render(); break; } @@ -1461,7 +1457,7 @@ void IRGeneratorForStatements::endVisit(FunctionCall const& _functionCall) templ("success", IRVariable(_functionCall).commaSeparatedList()); templ("isTransfer", functionType->kind() == FunctionType::Kind::Transfer); templ("forwardingRevert", m_utils.forwardingRevertFunction()); - m_code << templ.render(); + appendCode() << templ.render(); break; } @@ -1524,7 +1520,7 @@ void IRGeneratorForStatements::endVisit(FunctionCall const& _functionCall) templ("gas", "sub(gas(), " + formatNumber(gasNeededByCaller) + ")"); } - m_code << templ.render(); + appendCode() << templ.render(); break; } @@ -1585,28 +1581,13 @@ void IRGeneratorForStatements::endVisit(MemberAccess const& _memberAccess) if (memberFunctionType && memberFunctionType->bound()) { - solAssert((set{ - Type::Category::Contract, - Type::Category::Bool, - Type::Category::Integer, - Type::Category::Address, - Type::Category::Function, - Type::Category::Struct, - Type::Category::Enum, - Type::Category::Mapping, - Type::Category::Array, - Type::Category::FixedBytes, - }).count(objectCategory) > 0, ""); - define(IRVariable(_memberAccess).part("self"), _memberAccess.expression()); solAssert(*_memberAccess.annotation().requiredLookup == VirtualLookup::Static, ""); if (memberFunctionType->kind() == FunctionType::Kind::Internal) - { - auto const& functionDefinition = dynamic_cast(memberFunctionType->declaration()); - define(IRVariable(_memberAccess).part("functionIdentifier")) << to_string(functionDefinition.id()) << "\n"; - if (!_memberAccess.annotation().calledDirectly) - m_context.addToInternalDispatch(functionDefinition); - } + assignInternalFunctionIDIfNotCalledDirectly( + _memberAccess, + dynamic_cast(memberFunctionType->declaration()) + ); else if ( memberFunctionType->kind() == FunctionType::Kind::ArrayPush || memberFunctionType->kind() == FunctionType::Kind::ArrayPop @@ -1774,7 +1755,7 @@ void IRGeneratorForStatements::endVisit(MemberAccess const& _memberAccess) solAssert(!contractType.isSuper(), ""); ContractDefinition const& contract = contractType.contractDefinition(); m_context.subObjectsCreated().insert(&contract); - m_code << Whiskers(R"( + appendCode() << Whiskers(R"( let := datasize("") let := (add(, 32)) mstore(, ) @@ -1827,7 +1808,7 @@ void IRGeneratorForStatements::endVisit(MemberAccess const& _memberAccess) { pair const& offsets = structType.storageOffsetsOfMember(member); string slot = m_context.newYulVariable(); - m_code << "let " << slot << " := " << + appendCode() << "let " << slot << " := " << ("add(" + expression.part("slot").name() + ", " + offsets.first.str() + ")\n"); setLValue(_memberAccess, IRLValue{ type(_memberAccess), @@ -1838,7 +1819,7 @@ void IRGeneratorForStatements::endVisit(MemberAccess const& _memberAccess) case DataLocation::Memory: { string pos = m_context.newYulVariable(); - m_code << "let " << pos << " := " << + appendCode() << "let " << pos << " := " << ("add(" + expression.part("mpos").name() + ", " + structType.memoryOffsetOfMember(member).str() + ")\n"); setLValue(_memberAccess, IRLValue{ type(_memberAccess), @@ -1850,7 +1831,7 @@ void IRGeneratorForStatements::endVisit(MemberAccess const& _memberAccess) { string baseRef = expression.part("offset").name(); string offset = m_context.newYulVariable(); - m_code << "let " << offset << " := " << "add(" << baseRef << ", " << to_string(structType.calldataOffsetOfMember(member)) << ")\n"; + appendCode() << "let " << offset << " := " << "add(" << baseRef << ", " << to_string(structType.calldataOffsetOfMember(member)) << ")\n"; if (_memberAccess.annotation().type->isDynamicallyEncoded()) define(_memberAccess) << m_utils.accessCalldataTailFunction(*_memberAccess.annotation().type) << @@ -1944,11 +1925,9 @@ void IRGeneratorForStatements::endVisit(MemberAccess const& _memberAccess) *_memberAccess.annotation().referencedDeclaration ).resolveVirtual(m_context.mostDerivedContract(), super); - define(_memberAccess) << to_string(resolvedFunctionDef.id()) << "\n"; solAssert(resolvedFunctionDef.functionType(true), ""); solAssert(resolvedFunctionDef.functionType(true)->kind() == FunctionType::Kind::Internal, ""); - if (!_memberAccess.annotation().calledDirectly) - m_context.addToInternalDispatch(resolvedFunctionDef); + assignInternalFunctionIDIfNotCalledDirectly(_memberAccess, resolvedFunctionDef); } else if (auto const* variable = dynamic_cast(_memberAccess.annotation().referencedDeclaration)) handleVariableReference(*variable, _memberAccess); @@ -1960,11 +1939,7 @@ void IRGeneratorForStatements::endVisit(MemberAccess const& _memberAccess) break; case FunctionType::Kind::Internal: if (auto const* function = dynamic_cast(_memberAccess.annotation().referencedDeclaration)) - { - define(_memberAccess) << to_string(function->id()) << "\n"; - if (!_memberAccess.annotation().calledDirectly) - m_context.addToInternalDispatch(*function); - } + assignInternalFunctionIDIfNotCalledDirectly(_memberAccess, *function); else solAssert(false, "Function not found in member access"); break; @@ -2047,10 +2022,7 @@ void IRGeneratorForStatements::endVisit(MemberAccess const& _memberAccess) solAssert(funType->kind() == FunctionType::Kind::Internal, ""); solAssert(*_memberAccess.annotation().requiredLookup == VirtualLookup::Static, ""); - define(_memberAccess) << to_string(function->id()) << "\n"; - - if (!_memberAccess.annotation().calledDirectly) - m_context.addToInternalDispatch(*function); + assignInternalFunctionIDIfNotCalledDirectly(_memberAccess, *function); } else if (auto const* contract = dynamic_cast(_memberAccess.annotation().referencedDeclaration)) { @@ -2075,7 +2047,7 @@ bool IRGeneratorForStatements::visit(InlineAssembly const& _inlineAsm) solAssert(holds_alternative(modified), ""); // Do not provide dialect so that we get the full type information. - m_code << yul::AsmPrinter()(std::get(modified)) << "\n"; + appendCode() << yul::AsmPrinter()(std::get(modified)) << "\n"; return false; } @@ -2098,7 +2070,7 @@ void IRGeneratorForStatements::endVisit(IndexAccess const& _indexAccess) templ("indexAccess", m_utils.mappingIndexAccessFunction(mappingType, keyType)); templ("base", IRVariable(_indexAccess.baseExpression()).commaSeparatedList()); templ("key", IRVariable(*_indexAccess.indexExpression()).commaSeparatedList()); - m_code << templ.render(); + appendCode() << templ.render(); setLValue(_indexAccess, IRLValue{ *_indexAccess.annotation().type, IRLValue::Storage{ @@ -2126,7 +2098,7 @@ void IRGeneratorForStatements::endVisit(IndexAccess const& _indexAccess) string slot = m_context.newYulVariable(); string offset = m_context.newYulVariable(); - m_code << Whiskers(R"( + appendCode() << Whiskers(R"( let , := (, ) )") ("slot", slot) @@ -2193,7 +2165,7 @@ void IRGeneratorForStatements::endVisit(IndexAccess const& _indexAccess) IRVariable index{m_context.newYulVariable(), *TypeProvider::uint256()}; define(index, *_indexAccess.indexExpression()); - m_code << Whiskers(R"( + appendCode() << Whiskers(R"( if iszero(lt(, )) { () } let := (byte(, )) )") @@ -2294,12 +2266,10 @@ void IRGeneratorForStatements::endVisit(Identifier const& _identifier) { solAssert(*_identifier.annotation().requiredLookup == VirtualLookup::Virtual, ""); FunctionDefinition const& resolvedFunctionDef = functionDef->resolveVirtual(m_context.mostDerivedContract()); - define(_identifier) << to_string(resolvedFunctionDef.id()) << "\n"; solAssert(resolvedFunctionDef.functionType(true), ""); solAssert(resolvedFunctionDef.functionType(true)->kind() == FunctionType::Kind::Internal, ""); - if (!_identifier.annotation().calledDirectly) - m_context.addToInternalDispatch(resolvedFunctionDef); + assignInternalFunctionIDIfNotCalledDirectly(_identifier, resolvedFunctionDef); } else if (VariableDeclaration const* varDecl = dynamic_cast(declaration)) handleVariableReference(*varDecl, _identifier); @@ -2359,7 +2329,6 @@ void IRGeneratorForStatements::handleVariableReference( Expression const& _referencingExpression ) { - setLocation(_referencingExpression); if ((_variable.isStateVariable() || _variable.isFileLevelVariable()) && _variable.isConstant()) define(_referencingExpression) << constantValueFunction(_variable) << "()\n"; else if (_variable.isStateVariable() && _variable.immutable()) @@ -2429,11 +2398,10 @@ void IRGeneratorForStatements::appendExternalFunctionCall( // We could also just use MLOAD; POP right before the gas calculation, but the optimizer // would remove that, so we use MSTORE here. if (!funType.gasSet() && returnInfo.estimatedReturnSize > 0) - m_code << "mstore(add(" << m_utils.allocateUnboundedFunction() << "() , " << to_string(returnInfo.estimatedReturnSize) << "), 0)\n"; + appendCode() << "mstore(add(" << m_utils.allocateUnboundedFunction() << "() , " << to_string(returnInfo.estimatedReturnSize) << "), 0)\n"; } - Whiskers templ(R"( - if iszero(extcodesize(
)) { } + Whiskers templ(R"(if iszero(extcodesize(
)) { () } // storage for arguments and returned data let := () @@ -2458,7 +2426,7 @@ void IRGeneratorForStatements::appendExternalFunctionCall( := (, add(, )) } )"); - templ("revertNoCode", m_context.revertReasonIfDebug("Target contract does not contain code")); + templ("revertNoCode", m_utils.revertReasonIfDebugFunction("Target contract does not contain code")); templ("pos", m_context.newYulVariable()); templ("end", m_context.newYulVariable()); if (_functionCall.annotation().tryCall) @@ -2526,7 +2494,7 @@ void IRGeneratorForStatements::appendExternalFunctionCall( templ("forwardingRevert", m_utils.forwardingRevertFunction()); - m_code << templ.render(); + appendCode() << templ.render(); } void IRGeneratorForStatements::appendBareCall( @@ -2617,7 +2585,26 @@ void IRGeneratorForStatements::appendBareCall( templ("gas", "sub(gas(), " + formatNumber(gasNeededByCaller) + ")"); } - m_code << templ.render(); + appendCode() << templ.render(); +} + +void IRGeneratorForStatements::assignInternalFunctionIDIfNotCalledDirectly( + Expression const& _expression, + FunctionDefinition const& _referencedFunction +) +{ + solAssert( + dynamic_cast(&_expression) || + dynamic_cast(&_expression), + "" + ); + if (_expression.annotation().calledDirectly) + return; + + define(IRVariable(_expression).part("functionIdentifier")) << + to_string(m_context.internalFunctionID(_referencedFunction, false)) << + "\n"; + m_context.addToInternalDispatch(_referencedFunction); } IRVariable IRGeneratorForStatements::convert(IRVariable const& _from, Type const& _to) @@ -2649,14 +2636,14 @@ std::string IRGeneratorForStatements::expressionAsType(Expression const& _expres std::ostream& IRGeneratorForStatements::define(IRVariable const& _var) { if (_var.type().sizeOnStack() > 0) - m_code << "let " << _var.commaSeparatedList() << " := "; - return m_code; + appendCode() << "let " << _var.commaSeparatedList() << " := "; + return appendCode(false); } void IRGeneratorForStatements::declare(IRVariable const& _var) { if (_var.type().sizeOnStack() > 0) - m_code << "let " << _var.commaSeparatedList() << "\n"; + appendCode() << "let " << _var.commaSeparatedList() << "\n"; } void IRGeneratorForStatements::declareAssign(IRVariable const& _lhs, IRVariable const& _rhs, bool _declare) @@ -2667,15 +2654,15 @@ void IRGeneratorForStatements::declareAssign(IRVariable const& _lhs, IRVariable if (stackItemType) declareAssign(_lhs.part(stackItemName), _rhs.part(stackItemName), _declare); else - m_code << (_declare ? "let ": "") << _lhs.part(stackItemName).name() << " := " << _rhs.part(stackItemName).name() << "\n"; + appendCode() << (_declare ? "let ": "") << _lhs.part(stackItemName).name() << " := " << _rhs.part(stackItemName).name() << "\n"; else { if (_lhs.type().sizeOnStack() > 0) - m_code << + appendCode() << (_declare ? "let ": "") << _lhs.commaSeparatedList() << " := "; - m_code << m_context.utils().conversionFunction(_rhs.type(), _lhs.type()) << + appendCode() << m_context.utils().conversionFunction(_rhs.type(), _lhs.type()) << "(" << _rhs.commaSeparatedList() << ")\n"; @@ -2727,7 +2714,8 @@ string IRGeneratorForStatements::binaryOperation( solAssert( _type.category() == Type::Category::Integer || _type.category() == Type::Category::FixedBytes, - ""); + "" + ); switch (_operator) { case Token::BitOr: fun = "or"; break; @@ -2738,6 +2726,10 @@ string IRGeneratorForStatements::binaryOperation( } else if (TokenTraits::isArithmeticOp(_operator)) { + solUnimplementedAssert( + _type.category() != Type::Category::FixedPoint, + "Not yet implemented - FixedPointType." + ); IntegerType const* type = dynamic_cast(&_type); solAssert(type, ""); bool checked = m_context.arithmetic() == Arithmetic::Checked; @@ -2774,7 +2766,8 @@ std::string IRGeneratorForStatements::shiftOperation( ) { solUnimplementedAssert( - _amountToShift.type().category() != Type::Category::FixedPoint, + _amountToShift.type().category() != Type::Category::FixedPoint && + _value.type().category() != Type::Category::FixedPoint, "Not yet implemented - FixedPointType." ); IntegerType const* amountType = dynamic_cast(&_amountToShift.type()); @@ -2807,13 +2800,13 @@ void IRGeneratorForStatements::appendAndOrOperatorCode(BinaryOperation const& _b IRVariable value(_binOp); define(value, _binOp.leftExpression()); if (op == Token::Or) - m_code << "if iszero(" << value.name() << ") {\n"; + appendCode() << "if iszero(" << value.name() << ") {\n"; else - m_code << "if " << value.name() << " {\n"; + appendCode() << "if " << value.name() << " {\n"; _binOp.rightExpression().accept(*this); setLocation(_binOp); assign(value, _binOp.rightExpression()); - m_code << "}\n"; + appendCode() << "}\n"; } void IRGeneratorForStatements::writeToLValue(IRLValue const& _lvalue, IRVariable const& _value) @@ -2829,7 +2822,7 @@ void IRGeneratorForStatements::writeToLValue(IRLValue const& _lvalue, IRVariable [&](string const& _offset) { offsetArgument = ", " + _offset; } }, _storage.offset); - m_code << + appendCode() << m_utils.updateStorageValueFunction(_value.type(), _lvalue.type, offsetStatic) << "(" << _storage.slot << @@ -2847,23 +2840,30 @@ void IRGeneratorForStatements::writeToLValue(IRLValue const& _lvalue, IRVariable if (_memory.byteArrayElement) { solAssert(_lvalue.type == *TypeProvider::byte(), ""); - m_code << "mstore8(" + _memory.address + ", byte(0, " + prepared.commaSeparatedList() + "))\n"; + appendCode() << "mstore8(" + _memory.address + ", byte(0, " + prepared.commaSeparatedList() + "))\n"; } else - m_code << m_utils.writeToMemoryFunction(_lvalue.type) << + appendCode() << m_utils.writeToMemoryFunction(_lvalue.type) << "(" << _memory.address << ", " << prepared.commaSeparatedList() << ")\n"; } + else if (auto const* literalType = dynamic_cast(&_value.type())) + appendCode() << + m_utils.writeToMemoryFunction(*TypeProvider::uint256()) << + "(" << + _memory.address << + ", " << + m_utils.copyLiteralToMemoryFunction(literalType->value()) + "()" << + ")\n"; else { solAssert(_lvalue.type.sizeOnStack() == 1, ""); - solAssert(dynamic_cast(&_lvalue.type), ""); auto const* valueReferenceType = dynamic_cast(&_value.type()); solAssert(valueReferenceType && valueReferenceType->dataStoredIn(DataLocation::Memory), ""); - m_code << "mstore(" + _memory.address + ", " + _value.part("mpos").name() + ")\n"; + appendCode() << "mstore(" + _memory.address + ", " + _value.part("mpos").name() + ")\n"; } }, [&](IRLValue::Stack const& _stack) { assign(_stack.variable, _value); }, @@ -2877,7 +2877,7 @@ void IRGeneratorForStatements::writeToLValue(IRLValue const& _lvalue, IRVariable IRVariable prepared(m_context.newYulVariable(), _lvalue.type); define(prepared, _value); - m_code << "mstore(" << to_string(memOffset) << ", " << prepared.commaSeparatedList() << ")\n"; + appendCode() << "mstore(" << to_string(memOffset) << ", " << prepared.commaSeparatedList() << ")\n"; }, [&](IRLValue::Tuple const& _tuple) { auto components = std::move(_tuple.components); @@ -2970,36 +2970,36 @@ void IRGeneratorForStatements::generateLoop( { solAssert(_conditionExpression, "Expected condition for doWhile"); firstRun = m_context.newYulVariable(); - m_code << "let " << firstRun << " := 1\n"; + appendCode() << "let " << firstRun << " := 1\n"; } - m_code << "for {\n"; + appendCode() << "for {\n"; if (_initExpression) _initExpression->accept(*this); - m_code << "} 1 {\n"; + appendCode() << "} 1 {\n"; if (_loopExpression) _loopExpression->accept(*this); - m_code << "}\n"; - m_code << "{\n"; + appendCode() << "}\n"; + appendCode() << "{\n"; if (_conditionExpression) { if (_isDoWhile) - m_code << "if iszero(" << firstRun << ") {\n"; + appendCode() << "if iszero(" << firstRun << ") {\n"; _conditionExpression->accept(*this); - m_code << + appendCode() << "if iszero(" << expressionAsType(*_conditionExpression, *TypeProvider::boolean()) << ") { break }\n"; if (_isDoWhile) - m_code << "}\n" << firstRun << " := 0\n"; + appendCode() << "}\n" << firstRun << " := 0\n"; } _body.accept(*this); - m_code << "}\n"; + appendCode() << "}\n"; } Type const& IRGeneratorForStatements::type(Expression const& _expression) @@ -3014,9 +3014,9 @@ bool IRGeneratorForStatements::visit(TryStatement const& _tryStatement) externalCall.accept(*this); setLocation(_tryStatement); - m_code << "switch iszero(" << IRNames::trySuccessConditionVariable(externalCall) << ")\n"; + appendCode() << "switch iszero(" << IRNames::trySuccessConditionVariable(externalCall) << ")\n"; - m_code << "case 0 { // success case\n"; + appendCode() << "case 0 { // success case\n"; TryCatchClause const& successClause = *_tryStatement.clauses().front(); if (successClause.parameters()) { @@ -3034,32 +3034,34 @@ bool IRGeneratorForStatements::visit(TryStatement const& _tryStatement) successClause.block().accept(*this); setLocation(_tryStatement); - m_code << "}\n"; + appendCode() << "}\n"; - m_code << "default { // failure case\n"; + appendCode() << "default { // failure case\n"; handleCatch(_tryStatement); - m_code << "}\n"; + appendCode() << "}\n"; return false; } void IRGeneratorForStatements::handleCatch(TryStatement const& _tryStatement) { + setLocation(_tryStatement); string const runFallback = m_context.newYulVariable(); - m_code << "let " << runFallback << " := 1\n"; + appendCode() << "let " << runFallback << " := 1\n"; // This function returns zero on "short returndata". We have to add a success flag // once we implement custom error codes. if (_tryStatement.errorClause() || _tryStatement.panicClause()) - m_code << "switch " << m_utils.returnDataSelectorFunction() << "()\n"; + appendCode() << "switch " << m_utils.returnDataSelectorFunction() << "()\n"; if (TryCatchClause const* errorClause = _tryStatement.errorClause()) { - m_code << "case " << selectorFromSignature32("Error(string)") << " {\n"; + appendCode() << "case " << selectorFromSignature32("Error(string)") << " {\n"; + setLocation(*errorClause); string const dataVariable = m_context.newYulVariable(); - m_code << "let " << dataVariable << " := " << m_utils.tryDecodeErrorMessageFunction() << "()\n"; - m_code << "if " << dataVariable << " {\n"; - m_code << runFallback << " := 0\n"; + appendCode() << "let " << dataVariable << " := " << m_utils.tryDecodeErrorMessageFunction() << "()\n"; + appendCode() << "if " << dataVariable << " {\n"; + appendCode() << runFallback << " := 0\n"; if (errorClause->parameters()) { solAssert(errorClause->parameters()->parameters().size() == 1, ""); @@ -3067,17 +3069,20 @@ void IRGeneratorForStatements::handleCatch(TryStatement const& _tryStatement) define(var) << dataVariable << "\n"; } errorClause->accept(*this); - m_code << "}\n"; - m_code << "}\n"; + setLocation(*errorClause); + appendCode() << "}\n"; + setLocation(_tryStatement); + appendCode() << "}\n"; } if (TryCatchClause const* panicClause = _tryStatement.panicClause()) { - m_code << "case " << selectorFromSignature32("Panic(uint256)") << " {\n"; + appendCode() << "case " << selectorFromSignature32("Panic(uint256)") << " {\n"; + setLocation(*panicClause); string const success = m_context.newYulVariable(); string const code = m_context.newYulVariable(); - m_code << "let " << success << ", " << code << " := " << m_utils.tryDecodePanicDataFunction() << "()\n"; - m_code << "if " << success << " {\n"; - m_code << runFallback << " := 0\n"; + appendCode() << "let " << success << ", " << code << " := " << m_utils.tryDecodePanicDataFunction() << "()\n"; + appendCode() << "if " << success << " {\n"; + appendCode() << runFallback << " := 0\n"; if (panicClause->parameters()) { solAssert(panicClause->parameters()->parameters().size() == 1, ""); @@ -3085,20 +3090,25 @@ void IRGeneratorForStatements::handleCatch(TryStatement const& _tryStatement) define(var) << code << "\n"; } panicClause->accept(*this); - m_code << "}\n"; - m_code << "}\n"; + setLocation(*panicClause); + appendCode() << "}\n"; + setLocation(_tryStatement); + appendCode() << "}\n"; } - m_code << "if " << runFallback << " {\n"; + setLocation(_tryStatement); + appendCode() << "if " << runFallback << " {\n"; if (_tryStatement.fallbackClause()) handleCatchFallback(*_tryStatement.fallbackClause()); else - rethrow(); - m_code << "}\n"; + appendCode() << m_utils.forwardingRevertFunction() << "()\n"; + setLocation(_tryStatement); + appendCode() << "}\n"; } void IRGeneratorForStatements::handleCatchFallback(TryCatchClause const& _fallback) { + setLocation(_fallback); if (_fallback.parameters()) { solAssert(m_context.evmVersion().supportsReturndata(), ""); @@ -3115,17 +3125,6 @@ void IRGeneratorForStatements::handleCatchFallback(TryCatchClause const& _fallba _fallback.accept(*this); } -void IRGeneratorForStatements::rethrow() -{ - if (m_context.evmVersion().supportsReturndata()) - m_code << R"( - returndatacopy(0, 0, returndatasize()) - revert(0, returndatasize()) - )"s; - else - m_code << "revert(0, 0) // rethrow\n"s; -} - void IRGeneratorForStatements::revertWithError( string const& _signature, vector const& _parameterTypes, @@ -3154,7 +3153,7 @@ void IRGeneratorForStatements::revertWithError( templ("argumentVars", joinHumanReadablePrefixed(errorArgumentVars)); templ("encode", m_context.abiFunctions().tupleEncoder(errorArgumentTypes, _parameterTypes)); - m_code << templ.render(); + appendCode() << templ.render(); } @@ -3164,13 +3163,8 @@ bool IRGeneratorForStatements::visit(TryCatchClause const& _clause) return false; } -void IRGeneratorForStatements::setLocation(ASTNode const& _node) -{ - m_currentLocation = _node.location(); -} - string IRGeneratorForStatements::linkerSymbol(ContractDefinition const& _library) const { solAssert(_library.isLibrary(), ""); - return "linkersymbol(" + util::escapeAndQuoteString(_library.fullyQualifiedName()) + ")"; + return "linkersymbol(" + util::escapeAndQuoteYulString(_library.fullyQualifiedName()) + ")"; } diff --git a/libsolidity/codegen/ir/IRGeneratorForStatements.h b/libsolidity/codegen/ir/IRGeneratorForStatements.h index 90504aa7f..74b7def54 100644 --- a/libsolidity/codegen/ir/IRGeneratorForStatements.h +++ b/libsolidity/codegen/ir/IRGeneratorForStatements.h @@ -33,11 +33,33 @@ namespace solidity::frontend class IRGenerationContext; class YulUtilFunctions; +/** + * Base class for the statement generator. + * Encapsulates access to the yul code stream and handles source code locations. + */ +class IRGeneratorForStatementsBase: public ASTConstVisitor +{ +public: + IRGeneratorForStatementsBase(IRGenerationContext& _context): + m_context(_context) + {} + + virtual std::string code() const; + std::ostringstream& appendCode(bool _addLocationComment = true); +protected: + void setLocation(ASTNode const& _node); + langutil::SourceLocation m_currentLocation = {}; + langutil::SourceLocation m_lastLocation = {}; + IRGenerationContext& m_context; +private: + std::ostringstream m_code; +}; + /** * Component that translates Solidity's AST into Yul at statement level and below. * It is an AST visitor that appends to an internal string buffer. */ -class IRGeneratorForStatements: public ASTConstVisitor +class IRGeneratorForStatements: public IRGeneratorForStatementsBase { public: IRGeneratorForStatements( @@ -45,12 +67,12 @@ public: YulUtilFunctions& _utils, std::function _placeholderCallback = {} ): - m_context(_context), + IRGeneratorForStatementsBase(_context), m_placeholderCallback(std::move(_placeholderCallback)), m_utils(_utils) {} - std::string code() const; + std::string code() const override; /// Generate the code for the statements in the block; void generate(Block const& _block); @@ -83,7 +105,7 @@ public: bool visit(Continue const& _continueStatement) override; bool visit(Break const& _breakStatement) override; void endVisit(Return const& _return) override; - void endVisit(UnaryOperation const& _unaryOperation) override; + bool visit(UnaryOperation const& _unaryOperation) override; bool visit(BinaryOperation const& _binOp) override; void endVisit(FunctionCall const& _funCall) override; void endVisit(FunctionCallOptions const& _funCallOptions) override; @@ -103,9 +125,6 @@ private: void handleCatch(TryStatement const& _tryStatement); void handleCatchFallback(TryCatchClause const& _fallback); - /// Generates code to rethrow an exception. - void rethrow(); - /// Generates code to revert with an error. The error arguments are assumed to /// be already evaluated and available in local IRVariables, but not yet /// converted. @@ -134,6 +153,14 @@ private: std::vector> const& _arguments ); + /// Requests and assigns the internal ID of the referenced function to the referencing + /// expression and adds the function to the internal dispatch. + /// If the function is called right away, it does nothing. + void assignInternalFunctionIDIfNotCalledDirectly( + Expression const& _expression, + FunctionDefinition const& _referencedFunction + ); + /// Generates the required conversion code and @returns an IRVariable referring to the value of @a _variable /// converted to type @a _to. IRVariable convert(IRVariable const& _variable, Type const& _to); @@ -193,16 +220,11 @@ private: static Type const& type(Expression const& _expression); - void setLocation(ASTNode const& _node); - std::string linkerSymbol(ContractDefinition const& _library) const; - std::ostringstream m_code; - IRGenerationContext& m_context; std::function m_placeholderCallback; YulUtilFunctions& m_utils; std::optional m_currentLValue; - langutil::SourceLocation m_currentLocation; }; } diff --git a/libsolidity/formal/BMC.cpp b/libsolidity/formal/BMC.cpp index befefbb14..c7e25db29 100644 --- a/libsolidity/formal/BMC.cpp +++ b/libsolidity/formal/BMC.cpp @@ -18,7 +18,6 @@ #include -#include #include #include diff --git a/libsolidity/formal/CHC.cpp b/libsolidity/formal/CHC.cpp index 56066636b..23967ecd2 100644 --- a/libsolidity/formal/CHC.cpp +++ b/libsolidity/formal/CHC.cpp @@ -219,9 +219,20 @@ bool CHC::visit(FunctionDefinition const& _function) if (!m_currentContract) return false; - if (!_function.isImplemented()) + if ( + !_function.isImplemented() || + abstractAsNondet(_function) + ) { - addRule(summary(_function), "summary_function_" + to_string(_function.id())); + smtutil::Expression conj(true); + if ( + _function.stateMutability() == StateMutability::Pure || + _function.stateMutability() == StateMutability::View + ) + conj = conj && currentEqualInitialVarsConstraints(stateVariablesIncludingInheritedAndPrivate(_function)); + + conj = conj && errorFlag().currentValue() == 0; + addRule(smtutil::Expression::implies(conj, summary(_function)), "summary_function_" + to_string(_function.id())); return false; } @@ -262,7 +273,10 @@ void CHC::endVisit(FunctionDefinition const& _function) if (!m_currentContract) return; - if (!_function.isImplemented()) + if ( + !_function.isImplemented() || + abstractAsNondet(_function) + ) return; solAssert(m_currentFunction && m_currentContract, ""); @@ -759,10 +773,10 @@ void CHC::externalFunctionCall(FunctionCall const& _funCall) connectBlocks(m_currentBlock, predicate(*m_errorDest), errorFlag().currentValue() > 0); // To capture the possibility of a reentrant call, we record in the call graph that the current function // can call any of the external methods of the current contract. - solAssert(m_currentContract && m_currentFunction, ""); - for (auto const* definedFunction: contractFunctions(*m_currentContract)) - if (!definedFunction->isConstructor() && definedFunction->isPublic()) - m_callGraph[m_currentFunction].insert(definedFunction); + if (m_currentFunction) + for (auto const* definedFunction: contractFunctions(*m_currentContract)) + if (!definedFunction->isConstructor() && definedFunction->isPublic()) + m_callGraph[m_currentFunction].insert(definedFunction); m_context.addAssertion(errorFlag().currentValue() == 0); } @@ -774,7 +788,6 @@ void CHC::externalFunctionCallToTrustedCode(FunctionCall const& _funCall) auto kind = funType.kind(); solAssert(kind == FunctionType::Kind::External || kind == FunctionType::Kind::BareStaticCall, ""); - solAssert(m_currentContract, ""); auto function = functionCallToDefinition(_funCall, currentScopeContract(), m_currentContract); if (!function) return; @@ -932,8 +945,8 @@ void CHC::resetSourceAnalysis() if (!usesZ3) { auto smtlib2Interface = dynamic_cast(m_interface.get()); - smtlib2Interface->reset(); solAssert(smtlib2Interface, ""); + smtlib2Interface->reset(); m_context.setSolver(smtlib2Interface->smtlib2Interface()); } @@ -1001,6 +1014,37 @@ set CHC::transactionVerificationTargetsIds(ASTNode const* _txRoot) return verificationTargetsIds; } +optional CHC::natspecOptionFromString(string const& _option) +{ + static map options{ + {"abstract-function-nondet", CHCNatspecOption::AbstractFunctionNondet} + }; + if (options.count(_option)) + return options.at(_option); + return {}; +} + +set CHC::smtNatspecTags(FunctionDefinition const& _function) +{ + set options; + string smtStr = "custom:smtchecker"; + for (auto const& [tag, value]: _function.annotation().docTags) + if (tag == smtStr) + { + string const& content = value.content; + if (auto option = natspecOptionFromString(content)) + options.insert(*option); + else + m_errorReporter.warning(3130_error, _function.location(), "Unknown option for \"" + smtStr + "\": \"" + content + "\""); + } + return options; +} + +bool CHC::abstractAsNondet(FunctionDefinition const& _function) +{ + return smtNatspecTags(_function).count(CHCNatspecOption::AbstractFunctionNondet); +} + SortPointer CHC::sort(FunctionDefinition const& _function) { return functionBodySort(_function, m_currentContract, state()); @@ -1213,13 +1257,11 @@ smtutil::Expression CHC::initialConstraints(ContractDefinition const& _contract, { smtutil::Expression conj = state().state() == state().state(0); conj = conj && errorFlag().currentValue() == 0; - for (auto var: stateVariablesIncludingInheritedAndPrivate(_contract)) - conj = conj && m_context.variable(*var)->valueAtIndex(0) == currentValue(*var); + conj = conj && currentEqualInitialVarsConstraints(stateVariablesIncludingInheritedAndPrivate(_contract)); FunctionDefinition const* function = _function ? _function : _contract.constructor(); if (function) - for (auto var: function->parameters()) - conj = conj && m_context.variable(*var)->valueAtIndex(0) == currentValue(*var); + conj = conj && currentEqualInitialVarsConstraints(applyMap(function->parameters(), [](auto&& _var) -> VariableDeclaration const* { return _var.get(); })); return conj; } @@ -1254,6 +1296,13 @@ vector CHC::currentStateVariables(ContractDefinition const& return applyMap(SMTEncoder::stateVariablesIncludingInheritedAndPrivate(_contract), [this](auto _var) { return currentValue(*_var); }); } +smtutil::Expression CHC::currentEqualInitialVarsConstraints(vector const& _vars) const +{ + return fold(_vars, smtutil::Expression(true), [this](auto&& _conj, auto _var) { + return move(_conj) && currentValue(*_var) == m_context.variable(*_var)->valueAtIndex(0); + }); +} + string CHC::predicateName(ASTNode const* _node, ContractDefinition const* _contract) { string prefix; diff --git a/libsolidity/formal/CHC.h b/libsolidity/formal/CHC.h index 16513ccaa..7b6a50f14 100644 --- a/libsolidity/formal/CHC.h +++ b/libsolidity/formal/CHC.h @@ -70,6 +70,11 @@ public: /// the constructor. std::vector unhandledQueries() const; + enum class CHCNatspecOption + { + AbstractFunctionNondet + }; + private: /// Visitor functions. //@{ @@ -123,6 +128,19 @@ private: std::set transactionVerificationTargetsIds(ASTNode const* _txRoot); //@} + /// SMT Natspec and abstraction helpers. + //@{ + /// @returns a CHCNatspecOption enum if _option is a valid SMTChecker Natspec value + /// or nullopt otherwise. + static std::optional natspecOptionFromString(std::string const& _option); + /// @returns which SMTChecker options are enabled by @a _function's Natspec via + /// `@custom:smtchecker