Merge pull request #11616 from ethereum/issue-8191-yul

Test yul code blocks in documentation.
This commit is contained in:
chriseth 2021-07-08 15:37:01 +02:00 committed by GitHub
commit fec01c112a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 236 additions and 82 deletions

View File

@ -519,7 +519,7 @@ compact again at the end.
ExpressionSplitter ExpressionSplitter
^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^
The expression splitter turns expressions like ``add(mload(x), mul(mload(y), 0x20))`` The expression splitter turns expressions like ``add(mload(0x123), mul(mload(0x456), 0x20))``
into a sequence of declarations of unique variables that are assigned sub-expressions 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 of that expression so that each function call has only variables or literals
as arguments. as arguments.
@ -529,9 +529,9 @@ The above would be transformed into
.. code-block:: yul .. code-block:: yul
{ {
let _1 := mload(y) let _1 := mload(0x123)
let _2 := mul(_1, 0x20) let _2 := mul(_1, 0x20)
let _3 := mload(x) let _3 := mload(0x456)
let z := add(_3, _2) let z := add(_3, _2)
} }
@ -633,7 +633,7 @@ The SSA transform converts this snippet to the following:
{ {
let a_1 := 1 let a_1 := 1
a := a_1 let a := a_1
let a_2 := mload(a_1) let a_2 := mload(a_1)
a := a_2 a := a_2
let a_3 := sload(a_2) let a_3 := sload(a_2)
@ -1186,16 +1186,18 @@ The SSA transform rewrites
.. code-block:: yul .. code-block:: yul
a := E let a := calldataload(0)
mstore(a, 1) mstore(a, 1)
to to
.. code-block:: yul .. code-block:: yul
let a_1 := E let a_1 := calldataload(0)
a := a_1 let a := a_1
mstore(a_1, 1) mstore(a_1, 1)
let a_2 := calldataload(0x20)
a := a_2
The problem is that instead of ``a``, the variable ``a_1`` is used The problem is that instead of ``a``, the variable ``a_1`` is used
whenever ``a`` was referenced. The SSA transform changes statements whenever ``a`` was referenced. The SSA transform changes statements
@ -1204,9 +1206,11 @@ snippet is turned into
.. code-block:: yul .. code-block:: yul
a := E let a := calldataload(0)
let a_1 := a let a_1 := a
mstore(a_1, 1) mstore(a_1, 1)
a := calldataload(0x20)
let a_2 := a
This is a very simple equivalence transform, but when we now run the This is a very simple equivalence transform, but when we now run the
Common Subexpression Eliminator, it will replace all occurrences of ``a_1`` Common Subexpression Eliminator, it will replace all occurrences of ``a_1``

View File

@ -198,7 +198,8 @@ has to be specified after a colon:
.. code-block:: yul .. code-block:: yul
let x := and("abc":uint32, add(3:uint256, 2:uint256)) // This will not compile (u32 and u256 type not implemented yet)
let x := and("abc":u32, add(3:u256, 2:u256))
Function Calls Function Calls
@ -212,10 +213,9 @@ they have to be assigned to local variables.
.. code-block:: yul .. code-block:: yul
function f(x, y) -> a, b { /* ... */ }
mstore(0x80, add(mload(0x80), 3)) mstore(0x80, add(mload(0x80), 3))
// Here, the user-defined function `f` returns // Here, the user-defined function `f` returns two values.
// two values. The definition of the function
// is missing from the example.
let x, y := f(1, mload(0)) let x, y := f(1, mload(0))
For built-in functions of the EVM, functional expressions For built-in functions of the EVM, functional expressions
@ -271,9 +271,10 @@ that returns multiple values.
.. code-block:: yul .. code-block:: yul
// This will not compile (u32 and u256 type not implemented yet)
{ {
let zero:uint32 := 0:uint32 let zero:u32 := 0:u32
let v:uint256, t:uint32 := f() let v:u256, t:u32 := f()
let x, y := g() let x, y := g()
} }
@ -314,7 +315,7 @@ you need multiple alternatives.
.. code-block:: yul .. code-block:: yul
if eq(value, 0) { revert(0, 0) } if lt(calldatasize(), 4) { revert(0, 0) }
The curly braces for the body are required. The curly braces for the body are required.

View File

@ -19,6 +19,7 @@
# (c) 2016-2019 solidity contributors. # (c) 2016-2019 solidity contributors.
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
YULARGS=(--strict-assembly)
FULLARGS=(--optimize --ignore-missing --combined-json "abi,asm,ast,bin,bin-runtime,compact-format,devdoc,hashes,interface,metadata,opcodes,srcmap,srcmap-runtime,userdoc") FULLARGS=(--optimize --ignore-missing --combined-json "abi,asm,ast,bin,bin-runtime,compact-format,devdoc,hashes,interface,metadata,opcodes,srcmap,srcmap-runtime,userdoc")
OLDARGS=(--optimize --combined-json "abi,asm,ast,bin,bin-runtime,devdoc,interface,metadata,opcodes,srcmap,srcmap-runtime,userdoc") OLDARGS=(--optimize --combined-json "abi,asm,ast,bin,bin-runtime,devdoc,interface,metadata,opcodes,srcmap,srcmap-runtime,userdoc")
function compileFull() function compileFull()
@ -53,10 +54,18 @@ function compileFull()
local stderr_path; stderr_path=$(mktemp) local stderr_path; stderr_path=$(mktemp)
if [ "${files: -4}" == ".yul" ]
then
args=("${YULARGS[@]}")
fi
set +e set +e
"$SOLC" "${args[@]}" "${files[@]}" >/dev/null 2>"$stderr_path" "$SOLC" "${args[@]}" "${files[@]}" >/dev/null 2>"$stderr_path"
local exit_code=$? local exit_code=$?
local errors; errors=$(grep -v -E 'Warning: This is a pre-release compiler version|Warning: Experimental features are turned on|pragma experimental ABIEncoderV2|^ +--> |^ +\||^[0-9]+ +\|' < "$stderr_path") local errors; errors=$(grep -v -E \
-e 'Warning: This is a pre-release compiler version|Warning: Experimental features are turned on|pragma experimental ABIEncoderV2|^ +--> |^ +\||^[0-9]+ +\| ' \
-e 'Warning: Yul is still experimental. Please use the output with care.' < "$stderr_path")
set -e set -e
rm "$stderr_path" rm "$stderr_path"

View File

@ -10,8 +10,9 @@ import sys
import re import re
import os import os
import hashlib import hashlib
from os.path import join, isfile, split, basename from os.path import join, isfile, basename
from argparse import ArgumentParser from argparse import ArgumentParser
from textwrap import indent, dedent
def extract_test_cases(path): def extract_test_cases(path):
with open(path, encoding="utf8", errors='ignore', mode='r', newline='') as file: with open(path, encoding="utf8", errors='ignore', mode='r', newline='') as file:
@ -36,11 +37,42 @@ def extract_test_cases(path):
return tests return tests
# Extract code examples based on a start marker def extract_solidity_docs_cases(path):
tests = extract_docs_cases(path, [".. code-block:: solidity", '::'])
codeStart = "(// SPDX-License-Identifier:|pragma solidity|contract.*{|library.*{|interface.*{)"
# Filter out tests that are not supposed to be compilable.
return [
test.lstrip("\n")
for test in tests
if re.search(r'^\s{4}' + codeStart, test, re.MULTILINE) is not None
]
def extract_yul_docs_cases(path):
tests = extract_docs_cases(path, [".. code-block:: yul"])
def wrap_in_object(code):
for line in code.splitlines():
line = line.lstrip()
if line.startswith("//"):
continue
if not line.startswith("object") and not line.startswith("{"):
return indent("{{\n{}\n}}\n\n".format(code.rstrip()), " ")
break
return code
return [
wrap_in_object(test)
for test in tests
if test.strip() != ""
]
# Extract code examples based on the 'beginMarker' parameter
# up until we reach EOF or a line that is not empty and doesn't start with 4 # up until we reach EOF or a line that is not empty and doesn't start with 4
# spaces. # spaces.
def extract_docs_cases(path): def extract_docs_cases(path, beginMarkers):
beginMarkers = ['.. code-block:: solidity', '::']
immediatelyAfterMarker = False immediatelyAfterMarker = False
insideBlock = False insideBlock = False
tests = [] tests = []
@ -59,48 +91,45 @@ def extract_docs_cases(path):
if line == '' or line.startswith(" "): if line == '' or line.startswith(" "):
tests[-1] += line + "\n" tests[-1] += line + "\n"
immediatelyAfterMarker = False immediatelyAfterMarker = False
else: continue
insideBlock = False insideBlock = False
elif any(map(line.lower().startswith, beginMarkers)): if any(map(line.lower().startswith, beginMarkers)):
insideBlock = True insideBlock = True
immediatelyAfterMarker = True immediatelyAfterMarker = True
tests += [''] tests += ['']
codeStart = "(// SPDX-License-Identifier:|pragma solidity|contract.*{|library.*{|interface.*{)" return tests
for test in tests: def write_cases(f, solidityTests, yulTests):
if re.search(r'^\s{0,3}' + codeStart, test, re.MULTILINE):
print("Indentation error in " + path + ":")
print(test)
exit(1)
# Filter out tests that are not supposed to be compilable.
return [
test.lstrip("\n")
for test in tests
if re.search(r'^\s{4}' + codeStart, test, re.MULTILINE) is not None
]
def write_cases(f, tests):
cleaned_filename = f.replace(".","_").replace("-","_").replace(" ","_").lower() cleaned_filename = f.replace(".","_").replace("-","_").replace(" ","_").lower()
for test in tests: for language, test in [("sol", t) for t in solidityTests] + [("yul", t) for t in yulTests]:
# When code examples are extracted they are indented by 8 spaces, which violates the style guide, # When code examples are extracted they are indented by 8 spaces, which violates the style guide,
# so before checking remove 4 spaces from each line. # so before checking remove 4 spaces from each line.
remainder = re.sub(r'^ {4}', '', test, 0, re.MULTILINE) remainder = dedent(test)
sol_filename = 'test_%s_%s.sol' % (hashlib.sha256(test.encode("utf-8")).hexdigest(), cleaned_filename) sol_filename = 'test_%s_%s.%s' % (hashlib.sha256(test.encode("utf-8")).hexdigest(), cleaned_filename, language)
with open(sol_filename, mode='w', encoding='utf8', newline='') as fi: with open(sol_filename, mode='w', encoding='utf8', newline='') as fi:
fi.write(remainder) fi.write(remainder)
def extract_and_write(path): def extract_and_write(path, language):
assert language in ["solidity", "yul", ""]
yulCases = []
cases = []
if path.lower().endswith('.rst'): if path.lower().endswith('.rst'):
cases = extract_docs_cases(path) if language in ("solidity", ""):
cases = extract_solidity_docs_cases(path)
if language in ("yul", ""):
yulCases = extract_yul_docs_cases(path)
elif path.endswith('.sol'): elif path.endswith('.sol'):
if language in ("solidity", ""):
with open(path, mode='r', encoding='utf8', newline='') as f: with open(path, mode='r', encoding='utf8', newline='') as f:
cases = [f.read()] cases = [f.read()]
else: else:
cases = extract_test_cases(path) cases = extract_test_cases(path)
write_cases(basename(path), cases) write_cases(basename(path), cases, yulCases)
if __name__ == '__main__': if __name__ == '__main__':
script_description = ( script_description = (
@ -110,11 +139,19 @@ if __name__ == '__main__':
parser = ArgumentParser(description=script_description) parser = ArgumentParser(description=script_description)
parser.add_argument(dest='path', help='Path to file or directory to look for code in.') parser.add_argument(dest='path', help='Path to file or directory to look for code in.')
parser.add_argument(
'-l', '--language',
dest='language',
choices=["yul", "solidity"],
default="",
action='store',
help="Extract only code blocks in the given language"
)
options = parser.parse_args() options = parser.parse_args()
path = options.path path = options.path
if isfile(path): if isfile(path):
extract_and_write(path) extract_and_write(path, options.language)
else: else:
for root, subdirs, files in os.walk(path): for root, subdirs, files in os.walk(path):
if '_build' in subdirs: if '_build' in subdirs:
@ -125,4 +162,4 @@ if __name__ == '__main__':
if basename(f) == "invalid_utf8_sequence.sol": if basename(f) == "invalid_utf8_sequence.sol":
continue # ignore the test with broken utf-8 encoding continue # ignore the test with broken utf-8 encoding
path = join(root, f) path = join(root, f)
extract_and_write(path) extract_and_write(path, options.language)

View File

@ -364,7 +364,7 @@ SOLTMPDIR=$(mktemp -d)
"$REPO_ROOT"/scripts/isolate_tests.py "$REPO_ROOT"/docs/ "$REPO_ROOT"/scripts/isolate_tests.py "$REPO_ROOT"/docs/
developmentVersion=$("$REPO_ROOT/scripts/get_version.sh") developmentVersion=$("$REPO_ROOT/scripts/get_version.sh")
for f in *.sol for f in *.yul *.sol
do do
# The contributors guide uses syntax tests, but we cannot # The contributors guide uses syntax tests, but we cannot
# really handle them here. # really handle them here.

View File

@ -20,3 +20,25 @@ Some text
contract C {} contract C {}
More text. More text.
.. code-block:: yul
let x := add(1, 5)
.. code-block:: yul
// Yul code wrapped in object
{
{
let y := mul(3, 5)
}
}
.. code-block:: yul
// Yul code wrapped in named object
object "Test" {
{
let y := mul(6, 9)
}
}

View File

@ -46,3 +46,26 @@ Sphinx does not complain about these.
contract E {} contract E {}
More text. More text.
.. code-block:: yul
:force:
let x := add(1, 5)
.. code-block:: yul
:linenos:
:language: Yul
// Yul code wrapped in object
{
let y := mul(3, 5)
}
.. code-block:: yul
// Yul code wrapped in named object
object "Test" {
let y := mul(3, 5)
:linenos:
}

View File

@ -2,61 +2,119 @@
import unittest import unittest
from textwrap import dedent, indent
from unittest_helpers import FIXTURE_DIR, load_fixture from unittest_helpers import FIXTURE_DIR, load_fixture
# NOTE: This test file file only works with scripts/ added to PYTHONPATH so pylint can't find the imports # NOTE: This test file file only works with scripts/ added to PYTHONPATH so pylint can't find the imports
# pragma pylint: disable=import-error # pragma pylint: disable=import-error
from isolate_tests import extract_docs_cases from isolate_tests import extract_solidity_docs_cases, extract_yul_docs_cases
# pragma pylint: enable=import-error # pragma pylint: enable=import-error
CODE_BLOCK_RST_PATH = FIXTURE_DIR / 'code_block.rst' CODE_BLOCK_RST_PATH = FIXTURE_DIR / 'code_block.rst'
CODE_BLOCK_RST_CONTENT = load_fixture(CODE_BLOCK_RST_PATH) CODE_BLOCK_RST_CONTENT = load_fixture(CODE_BLOCK_RST_PATH)
CODE_BLOCK_WITH_DIRECTIVES_RST_PATH = FIXTURE_DIR / 'code_block_with_directives.rst' CODE_BLOCK_WITH_DIRECTIVES_RST_PATH = FIXTURE_DIR / 'code_block_with_directives.rst'
CODE_BLOCK_WITH_DIRECTIVES_RST_CONTENT = load_fixture(CODE_BLOCK_WITH_DIRECTIVES_RST_PATH) CODE_BLOCK_WITH_DIRECTIVES_RST_CONTENT = load_fixture(CODE_BLOCK_WITH_DIRECTIVES_RST_PATH)
def formatCase(text):
"""Formats code to contain only one indentation and terminate with a \n"""
return indent(dedent(text.lstrip("\n")), " ") + "\n"
class TestExtractDocsCases(unittest.TestCase): class TestExtractDocsCases(unittest.TestCase):
def setUp(self): def setUp(self):
self.maxDiff = 10000 self.maxDiff = 10000
def test_solidity_block(self): def test_solidity_block(self):
expected_cases = [ expected_cases = [formatCase(case) for case in [
" // SPDX-License-Identifier: GPL-3.0\n" """
" pragma solidity >=0.7.0 <0.9.0;\n" // SPDX-License-Identifier: GPL-3.0
"\n" pragma solidity >=0.7.0 <0.9.0;
" contract C {\n"
" function foo() public view {}\n"
" }\n"
"\n"
"\n",
" contract C {}\n" contract C {
"\n", function foo() public view {}
] }
self.assertEqual(extract_docs_cases(CODE_BLOCK_RST_PATH), expected_cases) """,
"""
contract C {}
""",
]]
self.assertEqual(extract_solidity_docs_cases(CODE_BLOCK_RST_PATH), expected_cases)
def test_solidity_block_with_directives(self): def test_solidity_block_with_directives(self):
expected_cases = [ expected_cases = [formatCase(case) for case in [
" // SPDX-License-Identifier: GPL-3.0\n" """
" pragma solidity >=0.7.0 <0.9.0;\n" // SPDX-License-Identifier: GPL-3.0
"\n" pragma solidity >=0.7.0 <0.9.0;
" contract C {\n"
" function foo() public view {}\n"
" }\n"
"\n"
"\n",
" contract C {}\n" contract C {
"\n", function foo() public view {}
}
" contract D {}\n" """,
" :linenos:\n" """
"\n", contract C {}
""",
"""
contract D {}
:linenos:
""",
"""
contract E {}
""",
]]
" contract E {}\n" self.assertEqual(extract_solidity_docs_cases(CODE_BLOCK_WITH_DIRECTIVES_RST_PATH), expected_cases)
"\n",
]
self.assertEqual(extract_docs_cases(CODE_BLOCK_WITH_DIRECTIVES_RST_PATH), expected_cases) def test_yul_block(self):
expected_cases = [formatCase(case) for case in [
"""
{
let x := add(1, 5)
}
""",
"""
// Yul code wrapped in object
{
{
let y := mul(3, 5)
}
}
""",
"""
// Yul code wrapped in named object
object "Test" {
{
let y := mul(6, 9)
}
}
""",
]]
self.assertEqual(extract_yul_docs_cases(CODE_BLOCK_RST_PATH), expected_cases)
def test_yul_block_with_directives(self):
expected_cases = [formatCase(case) for case in [
"""
{
let x := add(1, 5)
}
""",
"""
// Yul code wrapped in object
{
let y := mul(3, 5)
}
""",
"""
// Yul code wrapped in named object
object "Test" {
let y := mul(3, 5)
:linenos:
}
""",
]]
self.assertEqual(extract_yul_docs_cases(CODE_BLOCK_WITH_DIRECTIVES_RST_PATH), expected_cases)