diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 8bd03de79b..10312413e9 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -67,8 +67,9 @@ jobs:
matrix:
python-version: [["3.11", "311"]]
opt-mode: ["gas", "none", "codesize"]
- evm-version: [shanghai]
debug: [true, false]
+ evm-version: [shanghai]
+ experimental-codegen: [false]
memorymock: [false]
# https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs#expanding-or-adding-matrix-configurations
@@ -94,6 +95,14 @@ jobs:
opt-mode: gas
evm-version: cancun
+ # test experimental pipeline
+ - python-version: ["3.11", "311"]
+ opt-mode: gas
+ debug: false
+ evm-version: shanghai
+ experimental-codegen: true
+ # TODO: test experimental_codegen + -Ocodesize
+
# run with `--memorymock`, but only need to do it one configuration
# TODO: consider removing the memorymock tests
- python-version: ["3.11", "311"]
@@ -108,12 +117,14 @@ jobs:
opt-mode: gas
debug: false
evm-version: shanghai
+
- python-version: ["3.12", "312"]
opt-mode: gas
debug: false
evm-version: shanghai
- name: py${{ matrix.python-version[1] }}-opt-${{ matrix.opt-mode }}${{ matrix.debug && '-debug' || '' }}${{ matrix.memorymock && '-memorymock' || '' }}-${{ matrix.evm-version }}
+
+ name: py${{ matrix.python-version[1] }}-opt-${{ matrix.opt-mode }}${{ matrix.debug && '-debug' || '' }}${{ matrix.memorymock && '-memorymock' || '' }}${{ matrix.experimental-codegen && '-experimental' || '' }}-${{ matrix.evm-version }}
steps:
- uses: actions/checkout@v4
@@ -141,6 +152,7 @@ jobs:
--evm-version ${{ matrix.evm-version }} \
${{ matrix.debug && '--enable-compiler-debug-mode' || '' }} \
${{ matrix.memorymock && '--memorymock' || '' }} \
+ ${{ matrix.experimental-codegen && '--experimental-codegen' || '' }} \
--cov-branch \
--cov-report xml:coverage.xml \
--cov=vyper \
@@ -193,8 +205,7 @@ jobs:
# NOTE: if the tests get poorly distributed, run this and commit the resulting `.test_durations` file to the `vyper-test-durations` repo.
# `pytest -m "fuzzing" --store-durations -r aR tests/`
- name: Fetch test-durations
- run: |
- curl --location "https://raw.githubusercontent.com/vyperlang/vyper-test-durations/master/test_durations" -o .test_durations
+ run: curl --location "https://raw.githubusercontent.com/vyperlang/vyper-test-durations/master/test_durations" -o .test_durations
- name: Run tests
run: |
diff --git a/setup.cfg b/setup.cfg
index 467c6a372b..f84c947981 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -31,3 +31,4 @@ testpaths = tests
xfail_strict = true
markers =
fuzzing: Run Hypothesis fuzz test suite (deselect with '-m "not fuzzing"')
+ venom_xfail: mark a test case as a regression (expected to fail) under the venom pipeline
diff --git a/tests/conftest.py b/tests/conftest.py
index 2e5f11b9b8..d0681cdf42 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -59,6 +59,7 @@ def pytest_addoption(parser):
help="change optimization mode",
)
parser.addoption("--enable-compiler-debug-mode", action="store_true")
+ parser.addoption("--experimental-codegen", action="store_true")
parser.addoption(
"--evm-version",
@@ -73,6 +74,8 @@ def output_formats():
output_formats = compiler.OUTPUT_FORMATS.copy()
del output_formats["bb"]
del output_formats["bb_runtime"]
+ del output_formats["cfg"]
+ del output_formats["cfg_runtime"]
return output_formats
@@ -89,6 +92,36 @@ def debug(pytestconfig):
_set_debug_mode(debug)
+@pytest.fixture(scope="session")
+def experimental_codegen(pytestconfig):
+ ret = pytestconfig.getoption("experimental_codegen")
+ assert isinstance(ret, bool)
+ return ret
+
+
+@pytest.fixture(autouse=True)
+def check_venom_xfail(request, experimental_codegen):
+ if not experimental_codegen:
+ return
+
+ marker = request.node.get_closest_marker("venom_xfail")
+ if marker is None:
+ return
+
+ # https://github.com/okken/pytest-runtime-xfail?tab=readme-ov-file#alternatives
+ request.node.add_marker(pytest.mark.xfail(strict=True, **marker.kwargs))
+
+
+@pytest.fixture
+def venom_xfail(request, experimental_codegen):
+ def _xfail(*args, **kwargs):
+ if not experimental_codegen:
+ return
+ request.node.add_marker(pytest.mark.xfail(*args, strict=True, **kwargs))
+
+ return _xfail
+
+
@pytest.fixture(scope="session", autouse=True)
def evm_version(pytestconfig):
# note: we configure the evm version that we emit code for,
@@ -108,6 +141,7 @@ def chdir_tmp_path(tmp_path):
yield
+# CMC 2024-03-01 this doesn't need to be a fixture
@pytest.fixture
def keccak():
return Web3.keccak
@@ -321,6 +355,7 @@ def _get_contract(
w3,
source_code,
optimize,
+ experimental_codegen,
output_formats,
*args,
override_opt_level=None,
@@ -329,6 +364,7 @@ def _get_contract(
):
settings = Settings()
settings.optimize = override_opt_level or optimize
+ settings.experimental_codegen = experimental_codegen
out = compiler.compile_code(
source_code,
# test that all output formats can get generated
@@ -352,17 +388,21 @@ def _get_contract(
@pytest.fixture(scope="module")
-def get_contract(w3, optimize, output_formats):
+def get_contract(w3, optimize, experimental_codegen, output_formats):
def fn(source_code, *args, **kwargs):
- return _get_contract(w3, source_code, optimize, output_formats, *args, **kwargs)
+ return _get_contract(
+ w3, source_code, optimize, experimental_codegen, output_formats, *args, **kwargs
+ )
return fn
@pytest.fixture
-def get_contract_with_gas_estimation(tester, w3, optimize, output_formats):
+def get_contract_with_gas_estimation(tester, w3, optimize, experimental_codegen, output_formats):
def get_contract_with_gas_estimation(source_code, *args, **kwargs):
- contract = _get_contract(w3, source_code, optimize, output_formats, *args, **kwargs)
+ contract = _get_contract(
+ w3, source_code, optimize, experimental_codegen, output_formats, *args, **kwargs
+ )
for abi_ in contract._classic_contract.functions.abi:
if abi_["type"] == "function":
set_decorator_to_contract_function(w3, tester, contract, source_code, abi_["name"])
@@ -372,15 +412,19 @@ def get_contract_with_gas_estimation(source_code, *args, **kwargs):
@pytest.fixture
-def get_contract_with_gas_estimation_for_constants(w3, optimize, output_formats):
+def get_contract_with_gas_estimation_for_constants(
+ w3, optimize, experimental_codegen, output_formats
+):
def get_contract_with_gas_estimation_for_constants(source_code, *args, **kwargs):
- return _get_contract(w3, source_code, optimize, output_formats, *args, **kwargs)
+ return _get_contract(
+ w3, source_code, optimize, experimental_codegen, output_formats, *args, **kwargs
+ )
return get_contract_with_gas_estimation_for_constants
@pytest.fixture(scope="module")
-def get_contract_module(optimize, output_formats):
+def get_contract_module(optimize, experimental_codegen, output_formats):
"""
This fixture is used for Hypothesis tests to ensure that
the same contract is called over multiple runs of the test.
@@ -393,16 +437,25 @@ def get_contract_module(optimize, output_formats):
w3.eth.set_gas_price_strategy(zero_gas_price_strategy)
def get_contract_module(source_code, *args, **kwargs):
- return _get_contract(w3, source_code, optimize, output_formats, *args, **kwargs)
+ return _get_contract(
+ w3, source_code, optimize, experimental_codegen, output_formats, *args, **kwargs
+ )
return get_contract_module
def _deploy_blueprint_for(
- w3, source_code, optimize, output_formats, initcode_prefix=ERC5202_PREFIX, **kwargs
+ w3,
+ source_code,
+ optimize,
+ experimental_codegen,
+ output_formats,
+ initcode_prefix=ERC5202_PREFIX,
+ **kwargs,
):
settings = Settings()
settings.optimize = optimize
+ settings.experimental_codegen = experimental_codegen
out = compiler.compile_code(
source_code,
output_formats=output_formats,
@@ -438,9 +491,11 @@ def factory(address):
@pytest.fixture(scope="module")
-def deploy_blueprint_for(w3, optimize, output_formats):
+def deploy_blueprint_for(w3, optimize, experimental_codegen, output_formats):
def deploy_blueprint_for(source_code, *args, **kwargs):
- return _deploy_blueprint_for(w3, source_code, optimize, output_formats, *args, **kwargs)
+ return _deploy_blueprint_for(
+ w3, source_code, optimize, experimental_codegen, output_formats, *args, **kwargs
+ )
return deploy_blueprint_for
diff --git a/tests/functional/builtins/codegen/test_abi_decode.py b/tests/functional/builtins/codegen/test_abi_decode.py
index d281851f8e..213738957b 100644
--- a/tests/functional/builtins/codegen/test_abi_decode.py
+++ b/tests/functional/builtins/codegen/test_abi_decode.py
@@ -3,7 +3,7 @@
import pytest
from eth.codecs import abi
-from vyper.exceptions import ArgumentException, StructureException
+from vyper.exceptions import ArgumentException, StackTooDeep, StructureException
TEST_ADDR = "0x" + b"".join(chr(i).encode("utf-8") for i in range(20)).hex()
@@ -196,6 +196,7 @@ def abi_decode(x: Bytes[{len}]) -> DynArray[DynArray[uint256, 3], 3]:
@pytest.mark.parametrize("args", nested_3d_array_args)
@pytest.mark.parametrize("unwrap_tuple", (True, False))
+@pytest.mark.venom_xfail(raises=StackTooDeep, reason="stack scheduler regression")
def test_abi_decode_nested_dynarray2(get_contract, args, unwrap_tuple):
if unwrap_tuple is True:
encoded = abi.encode("(uint256[][][])", (args,))
@@ -273,6 +274,7 @@ def foo(bs: Bytes[160]) -> (uint256, DynArray[uint256, 3]):
assert c.foo(encoded) == [2**256 - 1, bs]
+@pytest.mark.venom_xfail(raises=StackTooDeep, reason="stack scheduler regression")
def test_abi_decode_private_nested_dynarray(get_contract):
code = """
bytez: DynArray[DynArray[DynArray[uint256, 3], 3], 3]
diff --git a/tests/functional/builtins/codegen/test_abi_encode.py b/tests/functional/builtins/codegen/test_abi_encode.py
index f014c47a19..305c4b1356 100644
--- a/tests/functional/builtins/codegen/test_abi_encode.py
+++ b/tests/functional/builtins/codegen/test_abi_encode.py
@@ -3,6 +3,8 @@
import pytest
from eth.codecs import abi
+from vyper.exceptions import StackTooDeep
+
# @pytest.mark.parametrize("string", ["a", "abc", "abcde", "potato"])
def test_abi_encode(get_contract):
@@ -226,6 +228,7 @@ def abi_encode(
@pytest.mark.parametrize("args", nested_3d_array_args)
+@pytest.mark.venom_xfail(raises=StackTooDeep, reason="stack scheduler regression")
def test_abi_encode_nested_dynarray_2(get_contract, args):
code = """
@external
@@ -330,6 +333,7 @@ def foo(bs: DynArray[uint256, 3]) -> (uint256, Bytes[160]):
assert c.foo(bs) == [2**256 - 1, abi.encode("(uint256[])", (bs,))]
+@pytest.mark.venom_xfail(raises=StackTooDeep, reason="stack scheduler regression")
def test_abi_encode_private_nested_dynarray(get_contract):
code = """
bytez: Bytes[1696]
diff --git a/tests/functional/codegen/features/test_clampers.py b/tests/functional/codegen/features/test_clampers.py
index 578413a8f4..fe51c026fe 100644
--- a/tests/functional/codegen/features/test_clampers.py
+++ b/tests/functional/codegen/features/test_clampers.py
@@ -4,6 +4,7 @@
from eth.codecs import abi
from eth_utils import keccak
+from vyper.exceptions import StackTooDeep
from vyper.utils import int_bounds
@@ -506,6 +507,7 @@ def foo(b: DynArray[int128, 10]) -> DynArray[int128, 10]:
@pytest.mark.parametrize("value", [0, 1, -1, 2**127 - 1, -(2**127)])
+@pytest.mark.venom_xfail(raises=StackTooDeep, reason="stack scheduler regression")
def test_multidimension_dynarray_clamper_passing(w3, get_contract, value):
code = """
@external
diff --git a/tests/functional/codegen/features/test_constructor.py b/tests/functional/codegen/features/test_constructor.py
index 9146ace8a6..d96a889497 100644
--- a/tests/functional/codegen/features/test_constructor.py
+++ b/tests/functional/codegen/features/test_constructor.py
@@ -1,6 +1,8 @@
import pytest
from web3.exceptions import ValidationError
+from vyper.exceptions import StackTooDeep
+
def test_init_argument_test(get_contract_with_gas_estimation):
init_argument_test = """
@@ -163,6 +165,7 @@ def get_foo() -> uint256:
assert c.get_foo() == 39
+@pytest.mark.venom_xfail(raises=StackTooDeep, reason="stack scheduler regression")
def test_nested_dynamic_array_constructor_arg_2(w3, get_contract_with_gas_estimation):
code = """
foo: int128
@@ -208,6 +211,7 @@ def get_foo() -> DynArray[DynArray[uint256, 3], 3]:
assert c.get_foo() == [[37, 41, 73], [37041, 41073, 73037], [146, 123, 148]]
+@pytest.mark.venom_xfail(raises=StackTooDeep, reason="stack scheduler regression")
def test_initialise_nested_dynamic_array_2(w3, get_contract_with_gas_estimation):
code = """
foo: DynArray[DynArray[DynArray[int128, 3], 3], 3]
diff --git a/tests/functional/codegen/features/test_immutable.py b/tests/functional/codegen/features/test_immutable.py
index 49ff54b353..874600633a 100644
--- a/tests/functional/codegen/features/test_immutable.py
+++ b/tests/functional/codegen/features/test_immutable.py
@@ -1,6 +1,7 @@
import pytest
from vyper.compiler.settings import OptimizationLevel
+from vyper.exceptions import StackTooDeep
@pytest.mark.parametrize(
@@ -198,6 +199,7 @@ def get_idx_two() -> uint256:
assert c.get_idx_two() == expected_values[2][2]
+@pytest.mark.venom_xfail(raises=StackTooDeep, reason="stack scheduler regression")
def test_nested_dynarray_immutable(get_contract):
code = """
my_list: immutable(DynArray[DynArray[DynArray[int128, 3], 3], 3])
diff --git a/tests/functional/codegen/types/test_dynamic_array.py b/tests/functional/codegen/types/test_dynamic_array.py
index b55f07639b..efa2799480 100644
--- a/tests/functional/codegen/types/test_dynamic_array.py
+++ b/tests/functional/codegen/types/test_dynamic_array.py
@@ -8,6 +8,7 @@
ArrayIndexException,
ImmutableViolation,
OverflowException,
+ StackTooDeep,
StateAccessViolation,
TypeMismatch,
)
@@ -60,6 +61,7 @@ def loo(x: DynArray[DynArray[int128, 2], 2]) -> int128:
print("Passed list tests")
+@pytest.mark.venom_xfail(raises=StackTooDeep, reason="stack scheduler regression")
def test_string_list(get_contract):
code = """
@external
@@ -732,6 +734,7 @@ def test_array_decimal_return3() -> DynArray[DynArray[decimal, 2], 2]:
assert c.test_array_decimal_return3() == [[1.0, 2.0], [3.0]]
+@pytest.mark.venom_xfail(raises=StackTooDeep, reason="stack scheduler regression")
def test_mult_list(get_contract_with_gas_estimation):
code = """
nest3: DynArray[DynArray[DynArray[uint256, 2], 2], 2]
@@ -1478,6 +1481,7 @@ def foo(x: int128) -> int128:
assert c.foo(7) == 392
+@pytest.mark.venom_xfail(raises=StackTooDeep, reason="stack scheduler regression")
def test_struct_of_lists(get_contract):
code = """
struct Foo:
@@ -1566,6 +1570,7 @@ def bar(x: int128) -> DynArray[int128, 3]:
assert c.bar(7) == [7, 14]
+@pytest.mark.venom_xfail(raises=StackTooDeep, reason="stack scheduler regression")
def test_nested_struct_of_lists(get_contract, assert_compile_failed, optimize):
code = """
struct nestedFoo:
@@ -1695,7 +1700,9 @@ def __init__():
("DynArray[DynArray[DynArray[uint256, 5], 5], 5]", [[[], []], []]),
],
)
-def test_empty_nested_dynarray(get_contract, typ, val):
+def test_empty_nested_dynarray(get_contract, typ, val, venom_xfail):
+ if val == [[[], []], []]:
+ venom_xfail(raises=StackTooDeep, reason="stack scheduler regression")
code = f"""
@external
def foo() -> {typ}:
diff --git a/tests/functional/examples/factory/test_factory.py b/tests/functional/examples/factory/test_factory.py
index 0c5cf61b04..18f6222c20 100644
--- a/tests/functional/examples/factory/test_factory.py
+++ b/tests/functional/examples/factory/test_factory.py
@@ -31,12 +31,14 @@ def create_exchange(token, factory):
@pytest.fixture
-def factory(get_contract, optimize):
+def factory(get_contract, optimize, experimental_codegen):
with open("examples/factory/Exchange.vy") as f:
code = f.read()
exchange_interface = vyper.compile_code(
- code, output_formats=["bytecode_runtime"], settings=Settings(optimize=optimize)
+ code,
+ output_formats=["bytecode_runtime"],
+ settings=Settings(optimize=optimize, experimental_codegen=experimental_codegen),
)
exchange_deployed_bytecode = exchange_interface["bytecode_runtime"]
diff --git a/tests/functional/syntax/test_address_code.py b/tests/functional/syntax/test_address_code.py
index 6556fc90b9..6be50a509b 100644
--- a/tests/functional/syntax/test_address_code.py
+++ b/tests/functional/syntax/test_address_code.py
@@ -161,7 +161,7 @@ def test_address_code_compile_success(code: str):
compiler.compile_code(code)
-def test_address_code_self_success(get_contract, optimize):
+def test_address_code_self_success(get_contract, optimize, experimental_codegen):
code = """
code_deployment: public(Bytes[32])
@@ -174,7 +174,7 @@ def code_runtime() -> Bytes[32]:
return slice(self.code, 0, 32)
"""
contract = get_contract(code)
- settings = Settings(optimize=optimize)
+ settings = Settings(optimize=optimize, experimental_codegen=experimental_codegen)
code_compiled = compiler.compile_code(
code, output_formats=["bytecode", "bytecode_runtime"], settings=settings
)
diff --git a/tests/functional/syntax/test_codehash.py b/tests/functional/syntax/test_codehash.py
index d351981946..7aa01a68e9 100644
--- a/tests/functional/syntax/test_codehash.py
+++ b/tests/functional/syntax/test_codehash.py
@@ -3,7 +3,7 @@
from vyper.utils import keccak256
-def test_get_extcodehash(get_contract, optimize):
+def test_get_extcodehash(get_contract, optimize, experimental_codegen):
code = """
a: address
@@ -28,7 +28,7 @@ def foo3() -> bytes32:
def foo4() -> bytes32:
return self.a.codehash
"""
- settings = Settings(optimize=optimize)
+ settings = Settings(optimize=optimize, experimental_codegen=experimental_codegen)
compiled = compile_code(code, output_formats=["bytecode_runtime"], settings=settings)
bytecode = bytes.fromhex(compiled["bytecode_runtime"][2:])
hash_ = keccak256(bytecode)
diff --git a/tests/unit/cli/vyper_json/test_compile_json.py b/tests/unit/cli/vyper_json/test_compile_json.py
index 4fe2111f43..62a799db65 100644
--- a/tests/unit/cli/vyper_json/test_compile_json.py
+++ b/tests/unit/cli/vyper_json/test_compile_json.py
@@ -113,11 +113,13 @@ def test_keyerror_becomes_jsonerror(input_json):
def test_compile_json(input_json, input_bundle):
foo_input = input_bundle.load_file("contracts/foo.vy")
- # remove bb and bb_runtime from output formats
+ # remove venom related from output formats
# because they require venom (experimental)
output_formats = OUTPUT_FORMATS.copy()
del output_formats["bb"]
del output_formats["bb_runtime"]
+ del output_formats["cfg"]
+ del output_formats["cfg_runtime"]
foo = compile_from_file_input(
foo_input, output_formats=output_formats, input_bundle=input_bundle
)
diff --git a/tests/unit/compiler/asm/test_asm_optimizer.py b/tests/unit/compiler/asm/test_asm_optimizer.py
index ce32249202..5742f7c8df 100644
--- a/tests/unit/compiler/asm/test_asm_optimizer.py
+++ b/tests/unit/compiler/asm/test_asm_optimizer.py
@@ -95,7 +95,7 @@ def test_dead_code_eliminator(code):
assert all(ctor_only not in instr for instr in runtime_asm)
-def test_library_code_eliminator(make_input_bundle):
+def test_library_code_eliminator(make_input_bundle, experimental_codegen):
library = """
@internal
def unused1():
@@ -120,5 +120,6 @@ def foo():
res = compile_code(code, input_bundle=input_bundle, output_formats=["asm"])
asm = res["asm"]
assert "some_function()" in asm
+
assert "unused1()" not in asm
assert "unused2()" not in asm
diff --git a/tests/unit/compiler/venom/test_dominator_tree.py b/tests/unit/compiler/venom/test_dominator_tree.py
new file mode 100644
index 0000000000..dc27380796
--- /dev/null
+++ b/tests/unit/compiler/venom/test_dominator_tree.py
@@ -0,0 +1,73 @@
+from typing import Optional
+
+from vyper.exceptions import CompilerPanic
+from vyper.utils import OrderedSet
+from vyper.venom.analysis import calculate_cfg
+from vyper.venom.basicblock import IRBasicBlock, IRInstruction, IRLabel, IRLiteral, IRVariable
+from vyper.venom.dominators import DominatorTree
+from vyper.venom.function import IRFunction
+from vyper.venom.passes.make_ssa import MakeSSA
+
+
+def _add_bb(
+ ctx: IRFunction, label: IRLabel, cfg_outs: [IRLabel], bb: Optional[IRBasicBlock] = None
+) -> IRBasicBlock:
+ bb = bb if bb is not None else IRBasicBlock(label, ctx)
+ ctx.append_basic_block(bb)
+ cfg_outs_len = len(cfg_outs)
+ if cfg_outs_len == 0:
+ bb.append_instruction("stop")
+ elif cfg_outs_len == 1:
+ bb.append_instruction("jmp", cfg_outs[0])
+ elif cfg_outs_len == 2:
+ bb.append_instruction("jnz", IRLiteral(1), cfg_outs[0], cfg_outs[1])
+ else:
+ raise CompilerPanic("Invalid number of CFG outs")
+ return bb
+
+
+def _make_test_ctx():
+ lab = [IRLabel(str(i)) for i in range(0, 9)]
+
+ ctx = IRFunction(lab[1])
+
+ bb1 = ctx.basic_blocks[0]
+ bb1.append_instruction("jmp", lab[2])
+
+ _add_bb(ctx, lab[7], [])
+ _add_bb(ctx, lab[6], [lab[7], lab[2]])
+ _add_bb(ctx, lab[5], [lab[6], lab[3]])
+ _add_bb(ctx, lab[4], [lab[6]])
+ _add_bb(ctx, lab[3], [lab[5]])
+ _add_bb(ctx, lab[2], [lab[3], lab[4]])
+
+ return ctx
+
+
+def test_deminator_frontier_calculation():
+ ctx = _make_test_ctx()
+ bb1, bb2, bb3, bb4, bb5, bb6, bb7 = [ctx.get_basic_block(str(i)) for i in range(1, 8)]
+
+ calculate_cfg(ctx)
+ dom = DominatorTree.build_dominator_tree(ctx, bb1)
+ df = dom.dominator_frontiers
+
+ assert len(df[bb1]) == 0, df[bb1]
+ assert df[bb2] == OrderedSet({bb2}), df[bb2]
+ assert df[bb3] == OrderedSet({bb3, bb6}), df[bb3]
+ assert df[bb4] == OrderedSet({bb6}), df[bb4]
+ assert df[bb5] == OrderedSet({bb3, bb6}), df[bb5]
+ assert df[bb6] == OrderedSet({bb2}), df[bb6]
+ assert len(df[bb7]) == 0, df[bb7]
+
+
+def test_phi_placement():
+ ctx = _make_test_ctx()
+ bb1, bb2, bb3, bb4, bb5, bb6, bb7 = [ctx.get_basic_block(str(i)) for i in range(1, 8)]
+
+ x = IRVariable("%x")
+ bb1.insert_instruction(IRInstruction("mload", [IRLiteral(0)], x), 0)
+ bb2.insert_instruction(IRInstruction("add", [x, IRLiteral(1)], x), 0)
+ bb7.insert_instruction(IRInstruction("mstore", [x, IRLiteral(0)]), 0)
+
+ MakeSSA.run_pass(ctx, bb1)
diff --git a/tests/unit/compiler/venom/test_duplicate_operands.py b/tests/unit/compiler/venom/test_duplicate_operands.py
index 437185cc72..7cc58e6f5c 100644
--- a/tests/unit/compiler/venom/test_duplicate_operands.py
+++ b/tests/unit/compiler/venom/test_duplicate_operands.py
@@ -23,5 +23,4 @@ def test_duplicate_operands():
bb.append_instruction("stop")
asm = generate_assembly_experimental(ctx, optimize=OptimizationLevel.GAS)
-
- assert asm == ["PUSH1", 10, "DUP1", "DUP1", "DUP1", "ADD", "MUL", "STOP"]
+ assert asm == ["PUSH1", 10, "DUP1", "DUP1", "ADD", "MUL", "STOP"]
diff --git a/tests/unit/compiler/venom/test_liveness_simple_loop.py b/tests/unit/compiler/venom/test_liveness_simple_loop.py
new file mode 100644
index 0000000000..e725518179
--- /dev/null
+++ b/tests/unit/compiler/venom/test_liveness_simple_loop.py
@@ -0,0 +1,16 @@
+import vyper
+from vyper.compiler.settings import Settings
+
+source = """
+@external
+def foo(a: uint256):
+ _numBids: uint256 = 20
+ b: uint256 = 10
+
+ for i: uint256 in range(128):
+ b = 1 + _numBids
+"""
+
+
+def test_liveness_simple_loop():
+ vyper.compile_code(source, ["opcodes"], settings=Settings(experimental_codegen=True))
diff --git a/tests/unit/compiler/venom/test_make_ssa.py b/tests/unit/compiler/venom/test_make_ssa.py
new file mode 100644
index 0000000000..2a04dfc134
--- /dev/null
+++ b/tests/unit/compiler/venom/test_make_ssa.py
@@ -0,0 +1,48 @@
+from vyper.venom.analysis import calculate_cfg, calculate_liveness
+from vyper.venom.basicblock import IRBasicBlock, IRLabel
+from vyper.venom.function import IRFunction
+from vyper.venom.passes.make_ssa import MakeSSA
+
+
+def test_phi_case():
+ ctx = IRFunction(IRLabel("_global"))
+
+ bb = ctx.get_basic_block()
+
+ bb_cont = IRBasicBlock(IRLabel("condition"), ctx)
+ bb_then = IRBasicBlock(IRLabel("then"), ctx)
+ bb_else = IRBasicBlock(IRLabel("else"), ctx)
+ bb_if_exit = IRBasicBlock(IRLabel("if_exit"), ctx)
+ ctx.append_basic_block(bb_cont)
+ ctx.append_basic_block(bb_then)
+ ctx.append_basic_block(bb_else)
+ ctx.append_basic_block(bb_if_exit)
+
+ v = bb.append_instruction("mload", 64)
+ bb_cont.append_instruction("jnz", v, bb_then.label, bb_else.label)
+
+ bb_if_exit.append_instruction("add", v, 1, ret=v)
+ bb_if_exit.append_instruction("jmp", bb_cont.label)
+
+ bb_then.append_instruction("assert", bb_then.append_instruction("mload", 96))
+ bb_then.append_instruction("jmp", bb_if_exit.label)
+ bb_else.append_instruction("jmp", bb_if_exit.label)
+
+ bb.append_instruction("jmp", bb_cont.label)
+
+ calculate_cfg(ctx)
+ MakeSSA.run_pass(ctx, ctx.basic_blocks[0])
+ calculate_liveness(ctx)
+
+ condition_block = ctx.get_basic_block("condition")
+ assert len(condition_block.instructions) == 2
+
+ phi_inst = condition_block.instructions[0]
+ assert phi_inst.opcode == "phi"
+ assert phi_inst.operands[0].name == "_global"
+ assert phi_inst.operands[1].name == "%1"
+ assert phi_inst.operands[2].name == "if_exit"
+ assert phi_inst.operands[3].name == "%1"
+ assert phi_inst.output.name == "%1"
+ assert phi_inst.output.value != phi_inst.operands[1].value
+ assert phi_inst.output.value != phi_inst.operands[3].value
diff --git a/tests/unit/compiler/venom/test_multi_entry_block.py b/tests/unit/compiler/venom/test_multi_entry_block.py
index 6d8b074994..47f4b88707 100644
--- a/tests/unit/compiler/venom/test_multi_entry_block.py
+++ b/tests/unit/compiler/venom/test_multi_entry_block.py
@@ -39,10 +39,10 @@ def test_multi_entry_block_1():
assert ctx.normalized, "CFG should be normalized"
finish_bb = ctx.get_basic_block(finish_label.value)
- cfg_in = list(finish_bb.cfg_in.keys())
+ cfg_in = list(finish_bb.cfg_in)
assert cfg_in[0].label.value == "target", "Should contain target"
- assert cfg_in[1].label.value == "finish_split___global", "Should contain finish_split___global"
- assert cfg_in[2].label.value == "finish_split_block_1", "Should contain finish_split_block_1"
+ assert cfg_in[1].label.value == "__global_split_finish", "Should contain __global_split_finish"
+ assert cfg_in[2].label.value == "block_1_split_finish", "Should contain block_1_split_finish"
# more complicated one
@@ -91,10 +91,10 @@ def test_multi_entry_block_2():
assert ctx.normalized, "CFG should be normalized"
finish_bb = ctx.get_basic_block(finish_label.value)
- cfg_in = list(finish_bb.cfg_in.keys())
+ cfg_in = list(finish_bb.cfg_in)
assert cfg_in[0].label.value == "target", "Should contain target"
- assert cfg_in[1].label.value == "finish_split___global", "Should contain finish_split___global"
- assert cfg_in[2].label.value == "finish_split_block_1", "Should contain finish_split_block_1"
+ assert cfg_in[1].label.value == "__global_split_finish", "Should contain __global_split_finish"
+ assert cfg_in[2].label.value == "block_1_split_finish", "Should contain block_1_split_finish"
def test_multi_entry_block_with_dynamic_jump():
@@ -132,7 +132,7 @@ def test_multi_entry_block_with_dynamic_jump():
assert ctx.normalized, "CFG should be normalized"
finish_bb = ctx.get_basic_block(finish_label.value)
- cfg_in = list(finish_bb.cfg_in.keys())
+ cfg_in = list(finish_bb.cfg_in)
assert cfg_in[0].label.value == "target", "Should contain target"
- assert cfg_in[1].label.value == "finish_split___global", "Should contain finish_split___global"
- assert cfg_in[2].label.value == "finish_split_block_1", "Should contain finish_split_block_1"
+ assert cfg_in[1].label.value == "__global_split_finish", "Should contain __global_split_finish"
+ assert cfg_in[2].label.value == "block_1_split_finish", "Should contain block_1_split_finish"
diff --git a/tests/unit/compiler/venom/test_variables.py b/tests/unit/compiler/venom/test_variables.py
new file mode 100644
index 0000000000..cded8d0e1a
--- /dev/null
+++ b/tests/unit/compiler/venom/test_variables.py
@@ -0,0 +1,8 @@
+from vyper.venom.basicblock import IRVariable
+
+
+def test_variable_equality():
+ v1 = IRVariable("%x")
+ v2 = IRVariable("%x")
+ assert v1 == v2
+ assert v1 != IRVariable("%y")
diff --git a/vyper/ast/pre_parser.py b/vyper/ast/pre_parser.py
index 227b639ad5..f0c339cca7 100644
--- a/vyper/ast/pre_parser.py
+++ b/vyper/ast/pre_parser.py
@@ -203,6 +203,12 @@ def pre_parse(code: str) -> tuple[Settings, ModificationOffsets, dict, str]:
if evm_version not in EVM_VERSIONS:
raise StructureException("Invalid evm version: `{evm_version}`", start)
settings.evm_version = evm_version
+ elif pragma.startswith("experimental-codegen"):
+ if settings.experimental_codegen is not None:
+ raise StructureException(
+ "pragma experimental-codegen specified twice!", start
+ )
+ settings.experimental_codegen = True
else:
raise StructureException(f"Unknown pragma `{pragma.split()[0]}`")
diff --git a/vyper/codegen/function_definitions/external_function.py b/vyper/codegen/function_definitions/external_function.py
index 6f783bb9c5..fe706699bb 100644
--- a/vyper/codegen/function_definitions/external_function.py
+++ b/vyper/codegen/function_definitions/external_function.py
@@ -154,10 +154,6 @@ def _adjust_gas_estimate(func_t, common_ir):
common_ir.add_gas_estimate += mem_expansion_cost
func_t._ir_info.gas_estimate = common_ir.gas
- # pass metadata through for venom pipeline:
- common_ir.passthrough_metadata["func_t"] = func_t
- common_ir.passthrough_metadata["frame_info"] = frame_info
-
def generate_ir_for_external_function(code, compilation_target):
# TODO type hints:
diff --git a/vyper/codegen/function_definitions/internal_function.py b/vyper/codegen/function_definitions/internal_function.py
index 0cf9850b70..cde1ec5c87 100644
--- a/vyper/codegen/function_definitions/internal_function.py
+++ b/vyper/codegen/function_definitions/internal_function.py
@@ -80,10 +80,6 @@ def generate_ir_for_internal_function(
# tag gas estimate and frame info
func_t._ir_info.gas_estimate = ir_node.gas
- frame_info = tag_frame_info(func_t, context)
-
- # pass metadata through for venom pipeline:
- ir_node.passthrough_metadata["frame_info"] = frame_info
- ir_node.passthrough_metadata["func_t"] = func_t
+ tag_frame_info(func_t, context)
return InternalFuncIR(ir_node)
diff --git a/vyper/codegen/return_.py b/vyper/codegen/return_.py
index da585ff0a1..a8dac640db 100644
--- a/vyper/codegen/return_.py
+++ b/vyper/codegen/return_.py
@@ -42,7 +42,6 @@ def finalize(fill_return_buffer):
# NOTE: because stack analysis is incomplete, cleanup_repeat must
# come after fill_return_buffer otherwise the stack will break
jump_to_exit_ir = IRnode.from_list(jump_to_exit)
- jump_to_exit_ir.passthrough_metadata["func_t"] = func_t
return IRnode.from_list(["seq", fill_return_buffer, cleanup_loops, jump_to_exit_ir])
if context.return_type is None:
diff --git a/vyper/codegen/self_call.py b/vyper/codegen/self_call.py
index f53e4a81b4..2363de3641 100644
--- a/vyper/codegen/self_call.py
+++ b/vyper/codegen/self_call.py
@@ -112,6 +112,4 @@ def ir_for_self_call(stmt_expr, context):
add_gas_estimate=func_t._ir_info.gas_estimate,
)
o.is_self_call = True
- o.passthrough_metadata["func_t"] = func_t
- o.passthrough_metadata["args_ir"] = args_ir
return o
diff --git a/vyper/compiler/__init__.py b/vyper/compiler/__init__.py
index ee909a57d4..84aea73071 100644
--- a/vyper/compiler/__init__.py
+++ b/vyper/compiler/__init__.py
@@ -25,6 +25,8 @@
"interface": output.build_interface_output,
"bb": output.build_bb_output,
"bb_runtime": output.build_bb_runtime_output,
+ "cfg": output.build_cfg_output,
+ "cfg_runtime": output.build_cfg_runtime_output,
"ir": output.build_ir_output,
"ir_runtime": output.build_ir_runtime_output,
"ir_dict": output.build_ir_dict_output,
diff --git a/vyper/compiler/output.py b/vyper/compiler/output.py
index de8e34370d..f8beb9d11b 100644
--- a/vyper/compiler/output.py
+++ b/vyper/compiler/output.py
@@ -90,6 +90,14 @@ def build_bb_runtime_output(compiler_data: CompilerData) -> IRnode:
return compiler_data.venom_functions[1]
+def build_cfg_output(compiler_data: CompilerData) -> str:
+ return compiler_data.venom_functions[0].as_graph()
+
+
+def build_cfg_runtime_output(compiler_data: CompilerData) -> str:
+ return compiler_data.venom_functions[1].as_graph()
+
+
def build_ir_output(compiler_data: CompilerData) -> IRnode:
if compiler_data.show_gas_estimates:
IRnode.repr_show_gas = True
diff --git a/vyper/compiler/phases.py b/vyper/compiler/phases.py
index e343938021..d794185195 100644
--- a/vyper/compiler/phases.py
+++ b/vyper/compiler/phases.py
@@ -97,8 +97,6 @@ def __init__(
no_bytecode_metadata: bool, optional
Do not add metadata to bytecode. Defaults to False
"""
- # to force experimental codegen, uncomment:
- # settings.experimental_codegen = True
if isinstance(file_input, str):
file_input = FileInput(
diff --git a/vyper/exceptions.py b/vyper/exceptions.py
index 3897f0ea41..996a1ddbd9 100644
--- a/vyper/exceptions.py
+++ b/vyper/exceptions.py
@@ -386,6 +386,10 @@ class CodegenPanic(VyperInternalException):
"""Invalid code generated during codegen phase"""
+class StackTooDeep(CodegenPanic):
+ """Stack too deep""" # (should not happen)
+
+
class UnexpectedNodeType(VyperInternalException):
"""Unexpected AST node type."""
diff --git a/vyper/ir/compile_ir.py b/vyper/ir/compile_ir.py
index c6b1ec396c..9133b12e90 100644
--- a/vyper/ir/compile_ir.py
+++ b/vyper/ir/compile_ir.py
@@ -1033,7 +1033,11 @@ def _stack_peephole_opts(assembly):
changed = True
del assembly[i]
continue
- if assembly[i : i + 2] == ["SWAP1", "SWAP1"]:
+ if (
+ isinstance(assembly[i], str)
+ and assembly[i].startswith("SWAP")
+ and assembly[i] == assembly[i + 1]
+ ):
changed = True
del assembly[i : i + 2]
if assembly[i] == "SWAP1" and assembly[i + 1].lower() in COMMUTATIVE_OPS:
diff --git a/vyper/utils.py b/vyper/utils.py
index ba615e58d7..114ddf97c2 100644
--- a/vyper/utils.py
+++ b/vyper/utils.py
@@ -15,7 +15,7 @@
_T = TypeVar("_T")
-class OrderedSet(Generic[_T], dict[_T, None]):
+class OrderedSet(Generic[_T]):
"""
a minimal "ordered set" class. this is needed in some places
because, while dict guarantees you can recover insertion order
@@ -25,57 +25,82 @@ class OrderedSet(Generic[_T], dict[_T, None]):
"""
def __init__(self, iterable=None):
- super().__init__()
+ self._data = dict()
if iterable is not None:
- for item in iterable:
- self.add(item)
+ self.update(iterable)
def __repr__(self):
- keys = ", ".join(repr(k) for k in self.keys())
+ keys = ", ".join(repr(k) for k in self)
return f"{{{keys}}}"
- def get(self, *args, **kwargs):
- raise RuntimeError("can't call get() on OrderedSet!")
+ def __iter__(self):
+ return iter(self._data)
+
+ def __contains__(self, item):
+ return self._data.__contains__(item)
+
+ def __len__(self):
+ return len(self._data)
def first(self):
return next(iter(self))
def add(self, item: _T) -> None:
- self[item] = None
+ self._data[item] = None
def remove(self, item: _T) -> None:
- del self[item]
+ del self._data[item]
+
+ def drop(self, item: _T):
+ # friendly version of remove
+ self._data.pop(item, None)
+
+ def dropmany(self, iterable):
+ for item in iterable:
+ self._data.pop(item, None)
def difference(self, other):
ret = self.copy()
- for k in other.keys():
- if k in ret:
- ret.remove(k)
+ ret.dropmany(other)
return ret
+ def update(self, other):
+ # CMC 2024-03-22 for some reason, this is faster than dict.update?
+ # (maybe size dependent)
+ for item in other:
+ self._data[item] = None
+
def union(self, other):
return self | other
- def update(self, other):
- super().update(self.__class__.fromkeys(other))
+ def __ior__(self, other):
+ self.update(other)
+ return self
def __or__(self, other):
- return self.__class__(super().__or__(other))
+ ret = self.copy()
+ ret.update(other)
+ return ret
+
+ def __eq__(self, other):
+ return self._data == other._data
def copy(self):
- return self.__class__(super().copy())
+ cls = self.__class__
+ ret = cls.__new__(cls)
+ ret._data = self._data.copy()
+ return ret
@classmethod
def intersection(cls, *sets):
- res = OrderedSet()
if len(sets) == 0:
raise ValueError("undefined: intersection of no sets")
- if len(sets) == 1:
- return sets[0].copy()
- for e in sets[0].keys():
- if all(e in s for s in sets[1:]):
- res.add(e)
- return res
+
+ ret = sets[0].copy()
+ for e in sets[0]:
+ if any(e not in s for s in sets[1:]):
+ ret.remove(e)
+ return ret
class StringEnum(enum.Enum):
diff --git a/vyper/venom/__init__.py b/vyper/venom/__init__.py
index d1c2d0c342..2efd58ad6c 100644
--- a/vyper/venom/__init__.py
+++ b/vyper/venom/__init__.py
@@ -11,10 +11,14 @@
ir_pass_optimize_unused_variables,
ir_pass_remove_unreachable_blocks,
)
+from vyper.venom.dominators import DominatorTree
from vyper.venom.function import IRFunction
from vyper.venom.ir_node_to_venom import ir_node_to_venom
from vyper.venom.passes.constant_propagation import ir_pass_constant_propagation
from vyper.venom.passes.dft import DFTPass
+from vyper.venom.passes.make_ssa import MakeSSA
+from vyper.venom.passes.normalization import NormalizationPass
+from vyper.venom.passes.simplify_cfg import SimplifyCFGPass
from vyper.venom.venom_to_assembly import VenomCompiler
DEFAULT_OPT_LEVEL = OptimizationLevel.default()
@@ -38,6 +42,24 @@ def generate_assembly_experimental(
def _run_passes(ctx: IRFunction, optimize: OptimizationLevel) -> None:
# Run passes on Venom IR
# TODO: Add support for optimization levels
+
+ ir_pass_optimize_empty_blocks(ctx)
+ ir_pass_remove_unreachable_blocks(ctx)
+
+ internals = [
+ bb
+ for bb in ctx.basic_blocks
+ if bb.label.value.startswith("internal") and len(bb.cfg_in) == 0
+ ]
+
+ SimplifyCFGPass.run_pass(ctx, ctx.basic_blocks[0])
+ for entry in internals:
+ SimplifyCFGPass.run_pass(ctx, entry)
+
+ MakeSSA.run_pass(ctx, ctx.basic_blocks[0])
+ for entry in internals:
+ MakeSSA.run_pass(ctx, entry)
+
while True:
changes = 0
@@ -51,7 +73,6 @@ def _run_passes(ctx: IRFunction, optimize: OptimizationLevel) -> None:
calculate_cfg(ctx)
calculate_liveness(ctx)
- changes += ir_pass_constant_propagation(ctx)
changes += DFTPass.run_pass(ctx)
calculate_cfg(ctx)
diff --git a/vyper/venom/analysis.py b/vyper/venom/analysis.py
index daebd2560c..066a60f45e 100644
--- a/vyper/venom/analysis.py
+++ b/vyper/venom/analysis.py
@@ -1,3 +1,5 @@
+from typing import Optional
+
from vyper.exceptions import CompilerPanic
from vyper.utils import OrderedSet
from vyper.venom.basicblock import (
@@ -38,6 +40,7 @@ def calculate_cfg(ctx: IRFunction) -> None:
def _reset_liveness(ctx: IRFunction) -> None:
for bb in ctx.basic_blocks:
+ bb.out_vars = OrderedSet()
for inst in bb.instructions:
inst.liveness = OrderedSet()
@@ -50,16 +53,15 @@ def _calculate_liveness(bb: IRBasicBlock) -> bool:
orig_liveness = bb.instructions[0].liveness.copy()
liveness = bb.out_vars.copy()
for instruction in reversed(bb.instructions):
- ops = instruction.get_inputs()
+ ins = instruction.get_inputs()
+ outs = instruction.get_outputs()
- for op in ops:
- if op in liveness:
- instruction.dup_requirements.add(op)
+ if ins or outs:
+ # perf: only copy if changed
+ liveness = liveness.copy()
+ liveness.update(ins)
+ liveness.dropmany(outs)
- liveness = liveness.union(OrderedSet.fromkeys(ops))
- out = instruction.get_outputs()[0] if len(instruction.get_outputs()) > 0 else None
- if out in liveness:
- liveness.remove(out)
instruction.liveness = liveness
return orig_liveness != bb.instructions[0].liveness
@@ -89,6 +91,18 @@ def calculate_liveness(ctx: IRFunction) -> None:
break
+def calculate_dup_requirements(ctx: IRFunction) -> None:
+ for bb in ctx.basic_blocks:
+ last_liveness = bb.out_vars
+ for inst in reversed(bb.instructions):
+ inst.dup_requirements = OrderedSet()
+ ops = inst.get_inputs()
+ for op in ops:
+ if op in last_liveness:
+ inst.dup_requirements.add(op)
+ last_liveness = inst.liveness
+
+
# calculate the input variables into self from source
def input_vars_from(source: IRBasicBlock, target: IRBasicBlock) -> OrderedSet[IRVariable]:
liveness = target.instructions[0].liveness.copy()
@@ -104,19 +118,17 @@ def input_vars_from(source: IRBasicBlock, target: IRBasicBlock) -> OrderedSet[IR
# will arbitrarily choose either %12 or %14 to be in the liveness
# set, and then during instruction selection, after this instruction,
# %12 will be replaced by %56 in the liveness set
- source1, source2 = inst.operands[0], inst.operands[2]
- phi1, phi2 = inst.operands[1], inst.operands[3]
- if source.label == source1:
- liveness.add(phi1)
- if phi2 in liveness:
- liveness.remove(phi2)
- elif source.label == source2:
- liveness.add(phi2)
- if phi1 in liveness:
- liveness.remove(phi1)
- else:
- # bad path into this phi node
- raise CompilerPanic(f"unreachable: {inst}")
+
+ # bad path into this phi node
+ if source.label not in inst.operands:
+ raise CompilerPanic(f"unreachable: {inst} from {source.label}")
+
+ for label, var in inst.phi_operands:
+ if label == source.label:
+ liveness.add(var)
+ else:
+ if var in liveness:
+ liveness.remove(var)
return liveness
@@ -137,8 +149,8 @@ def get_uses(self, op: IRVariable) -> list[IRInstruction]:
return self._dfg_inputs.get(op, [])
# the instruction which produces this variable.
- def get_producing_instruction(self, op: IRVariable) -> IRInstruction:
- return self._dfg_outputs[op]
+ def get_producing_instruction(self, op: IRVariable) -> Optional[IRInstruction]:
+ return self._dfg_outputs.get(op)
@classmethod
def build_dfg(cls, ctx: IRFunction) -> "DFG":
@@ -163,3 +175,20 @@ def build_dfg(cls, ctx: IRFunction) -> "DFG":
dfg._dfg_outputs[op] = inst
return dfg
+
+ def as_graph(self) -> str:
+ """
+ Generate a graphviz representation of the dfg
+ """
+ lines = ["digraph dfg_graph {"]
+ for var, inputs in self._dfg_inputs.items():
+ for input in inputs:
+ for op in input.get_outputs():
+ if isinstance(op, IRVariable):
+ lines.append(f' " {var.name} " -> " {op.name} "')
+
+ lines.append("}")
+ return "\n".join(lines)
+
+ def __repr__(self) -> str:
+ return self.as_graph()
diff --git a/vyper/venom/basicblock.py b/vyper/venom/basicblock.py
index ed70a5eaa0..6c509d8f95 100644
--- a/vyper/venom/basicblock.py
+++ b/vyper/venom/basicblock.py
@@ -1,10 +1,9 @@
-from enum import Enum, auto
-from typing import TYPE_CHECKING, Any, Iterator, Optional, Union
+from typing import TYPE_CHECKING, Any, Generator, Iterator, Optional, Union
from vyper.utils import OrderedSet
# instructions which can terminate a basic block
-BB_TERMINATORS = frozenset(["jmp", "djmp", "jnz", "ret", "return", "revert", "stop"])
+BB_TERMINATORS = frozenset(["jmp", "djmp", "jnz", "ret", "return", "revert", "stop", "exit"])
VOLATILE_INSTRUCTIONS = frozenset(
[
@@ -12,15 +11,22 @@
"alloca",
"call",
"staticcall",
+ "delegatecall",
"invoke",
"sload",
"sstore",
"iload",
"istore",
+ "tload",
+ "tstore",
"assert",
+ "assert_unreachable",
"mstore",
"mload",
"calldatacopy",
+ "mcopy",
+ "extcodecopy",
+ "returndatacopy",
"codecopy",
"dloadbytes",
"dload",
@@ -39,11 +45,15 @@
"istore",
"dloadbytes",
"calldatacopy",
+ "mcopy",
+ "returndatacopy",
"codecopy",
+ "extcodecopy",
"return",
"ret",
"revert",
"assert",
+ "assert_unreachable",
"selfdestruct",
"stop",
"invalid",
@@ -52,6 +62,7 @@
"djmp",
"jnz",
"log",
+ "exit",
]
)
@@ -87,6 +98,10 @@ class IROperand:
value: Any
+ @property
+ def name(self) -> str:
+ return self.value
+
class IRValue(IROperand):
"""
@@ -109,13 +124,16 @@ def __init__(self, value: int) -> None:
assert isinstance(value, int), "value must be an int"
self.value = value
- def __repr__(self) -> str:
- return str(self.value)
+ def __hash__(self) -> int:
+ return self.value.__hash__()
+ def __eq__(self, other) -> bool:
+ if not isinstance(other, type(self)):
+ return False
+ return self.value == other.value
-class MemType(Enum):
- OPERAND_STACK = auto()
- MEMORY = auto()
+ def __repr__(self) -> str:
+ return str(self.value)
class IRVariable(IRValue):
@@ -126,18 +144,34 @@ class IRVariable(IRValue):
value: str
offset: int = 0
- # some variables can be in memory for conversion from legacy IR to venom
- mem_type: MemType = MemType.OPERAND_STACK
- mem_addr: Optional[int] = None
-
- def __init__(
- self, value: str, mem_type: MemType = MemType.OPERAND_STACK, mem_addr: int = None
- ) -> None:
+ def __init__(self, value: str, version: Optional[str | int] = None) -> None:
assert isinstance(value, str)
+ assert ":" not in value, "Variable name cannot contain ':'"
+ if version:
+ assert isinstance(value, str) or isinstance(value, int), "value must be an str or int"
+ value = f"{value}:{version}"
+ if value[0] != "%":
+ value = f"%{value}"
self.value = value
self.offset = 0
- self.mem_type = mem_type
- self.mem_addr = mem_addr
+
+ @property
+ def name(self) -> str:
+ return self.value.split(":")[0]
+
+ @property
+ def version(self) -> int:
+ if ":" not in self.value:
+ return 0
+ return int(self.value.split(":")[1])
+
+ def __hash__(self) -> int:
+ return self.value.__hash__()
+
+ def __eq__(self, other) -> bool:
+ if not isinstance(other, type(self)):
+ return False
+ return self.value == other.value
def __repr__(self) -> str:
return self.value
@@ -158,6 +192,14 @@ def __init__(self, value: str, is_symbol: bool = False) -> None:
self.value = value
self.is_symbol = is_symbol
+ def __hash__(self) -> int:
+ return hash(self.value)
+
+ def __eq__(self, other) -> bool:
+ if not isinstance(other, type(self)):
+ return False
+ return self.value == other.value
+
def __repr__(self) -> str:
return self.value
@@ -182,6 +224,8 @@ class IRInstruction:
parent: Optional["IRBasicBlock"]
fence_id: int
annotation: Optional[str]
+ ast_source: Optional[int]
+ error_msg: Optional[str]
def __init__(
self,
@@ -200,6 +244,8 @@ def __init__(
self.parent = None
self.fence_id = -1
self.annotation = None
+ self.ast_source = None
+ self.error_msg = None
def get_label_operands(self) -> list[IRLabel]:
"""
@@ -246,22 +292,37 @@ def replace_label_operands(self, replacements: dict) -> None:
if isinstance(operand, IRLabel) and operand.value in replacements:
self.operands[i] = replacements[operand.value]
+ @property
+ def phi_operands(self) -> Generator[tuple[IRLabel, IRVariable], None, None]:
+ """
+ Get phi operands for instruction.
+ """
+ assert self.opcode == "phi", "instruction must be a phi"
+ for i in range(0, len(self.operands), 2):
+ label = self.operands[i]
+ var = self.operands[i + 1]
+ assert isinstance(label, IRLabel), "phi operand must be a label"
+ assert isinstance(var, IRVariable), "phi operand must be a variable"
+ yield label, var
+
def __repr__(self) -> str:
s = ""
if self.output:
s += f"{self.output} = "
opcode = f"{self.opcode} " if self.opcode != "store" else ""
s += opcode
- operands = ", ".join(
- [(f"label %{op}" if isinstance(op, IRLabel) else str(op)) for op in self.operands]
+ operands = self.operands
+ if opcode not in ["jmp", "jnz", "invoke"]:
+ operands = reversed(operands) # type: ignore
+ s += ", ".join(
+ [(f"label %{op}" if isinstance(op, IRLabel) else str(op)) for op in operands]
)
- s += operands
if self.annotation:
s += f" <{self.annotation}>"
- # if self.liveness:
- # return f"{s: <30} # {self.liveness}"
+ if self.liveness:
+ return f"{s: <30} # {self.liveness}"
return s
@@ -307,6 +368,9 @@ class IRBasicBlock:
# stack items which this basic block produces
out_vars: OrderedSet[IRVariable]
+ reachable: OrderedSet["IRBasicBlock"]
+ is_reachable: bool = False
+
def __init__(self, label: IRLabel, parent: "IRFunction") -> None:
assert isinstance(label, IRLabel), "label must be an IRLabel"
self.label = label
@@ -315,6 +379,8 @@ def __init__(self, label: IRLabel, parent: "IRFunction") -> None:
self.cfg_in = OrderedSet()
self.cfg_out = OrderedSet()
self.out_vars = OrderedSet()
+ self.reachable = OrderedSet()
+ self.is_reachable = False
def add_cfg_in(self, bb: "IRBasicBlock") -> None:
self.cfg_in.add(bb)
@@ -333,23 +399,26 @@ def remove_cfg_out(self, bb: "IRBasicBlock") -> None:
assert bb in self.cfg_out
self.cfg_out.remove(bb)
- @property
- def is_reachable(self) -> bool:
- return len(self.cfg_in) > 0
-
- def append_instruction(self, opcode: str, *args: Union[IROperand, int]) -> Optional[IRVariable]:
+ def append_instruction(
+ self, opcode: str, *args: Union[IROperand, int], ret: IRVariable = None
+ ) -> Optional[IRVariable]:
"""
Append an instruction to the basic block
Returns the output variable if the instruction supports one
"""
- ret = self.parent.get_next_variable() if opcode not in NO_OUTPUT_INSTRUCTIONS else None
+ assert not self.is_terminated, self
+
+ if ret is None:
+ ret = self.parent.get_next_variable() if opcode not in NO_OUTPUT_INSTRUCTIONS else None
# Wrap raw integers in IRLiterals
inst_args = [_ir_operand_from_value(arg) for arg in args]
inst = IRInstruction(opcode, inst_args, ret)
inst.parent = self
+ inst.ast_source = self.parent.ast_source
+ inst.error_msg = self.parent.error_msg
self.instructions.append(inst)
return ret
@@ -357,10 +426,9 @@ def append_invoke_instruction(
self, args: list[IROperand | int], returns: bool
) -> Optional[IRVariable]:
"""
- Append an instruction to the basic block
-
- Returns the output variable if the instruction supports one
+ Append an invoke to the basic block
"""
+ assert not self.is_terminated, self
ret = None
if returns:
ret = self.parent.get_next_variable()
@@ -368,16 +436,30 @@ def append_invoke_instruction(
# Wrap raw integers in IRLiterals
inst_args = [_ir_operand_from_value(arg) for arg in args]
+ assert isinstance(inst_args[0], IRLabel), "Invoked non label"
+
inst = IRInstruction("invoke", inst_args, ret)
inst.parent = self
+ inst.ast_source = self.parent.ast_source
+ inst.error_msg = self.parent.error_msg
self.instructions.append(inst)
return ret
- def insert_instruction(self, instruction: IRInstruction, index: int) -> None:
+ def insert_instruction(self, instruction: IRInstruction, index: Optional[int] = None) -> None:
assert isinstance(instruction, IRInstruction), "instruction must be an IRInstruction"
+
+ if index is None:
+ assert not self.is_terminated, self
+ index = len(self.instructions)
instruction.parent = self
+ instruction.ast_source = self.parent.ast_source
+ instruction.error_msg = self.parent.error_msg
self.instructions.insert(index, instruction)
+ def remove_instruction(self, instruction: IRInstruction) -> None:
+ assert isinstance(instruction, IRInstruction), "instruction must be an IRInstruction"
+ self.instructions.remove(instruction)
+
def clear_instructions(self) -> None:
self.instructions = []
@@ -388,6 +470,19 @@ def replace_operands(self, replacements: dict) -> None:
for instruction in self.instructions:
instruction.replace_operands(replacements)
+ def get_assignments(self):
+ """
+ Get all assignments in basic block.
+ """
+ return [inst.output for inst in self.instructions if inst.output]
+
+ @property
+ def is_empty(self) -> bool:
+ """
+ Check if the basic block is empty, i.e. it has no instructions.
+ """
+ return len(self.instructions) == 0
+
@property
def is_terminated(self) -> bool:
"""
@@ -399,6 +494,20 @@ def is_terminated(self) -> bool:
return False
return self.instructions[-1].opcode in BB_TERMINATORS
+ @property
+ def is_terminal(self) -> bool:
+ """
+ Check if the basic block is terminal.
+ """
+ return len(self.cfg_out) == 0
+
+ @property
+ def in_vars(self) -> OrderedSet[IRVariable]:
+ for inst in self.instructions:
+ if inst.opcode != "phi":
+ return inst.liveness
+ return OrderedSet()
+
def copy(self):
bb = IRBasicBlock(self.label, self.parent)
bb.instructions = self.instructions.copy()
diff --git a/vyper/venom/bb_optimizer.py b/vyper/venom/bb_optimizer.py
index 620ee66d15..60dd8bbee1 100644
--- a/vyper/venom/bb_optimizer.py
+++ b/vyper/venom/bb_optimizer.py
@@ -56,6 +56,25 @@ def _optimize_empty_basicblocks(ctx: IRFunction) -> int:
return count
+def _daisychain_empty_basicblocks(ctx: IRFunction) -> int:
+ count = 0
+ i = 0
+ while i < len(ctx.basic_blocks):
+ bb = ctx.basic_blocks[i]
+ i += 1
+ if bb.is_terminated:
+ continue
+
+ if i < len(ctx.basic_blocks) - 1:
+ bb.append_instruction("jmp", ctx.basic_blocks[i + 1].label)
+ else:
+ bb.append_instruction("stop")
+
+ count += 1
+
+ return count
+
+
@ir_pass
def ir_pass_optimize_empty_blocks(ctx: IRFunction) -> int:
changes = _optimize_empty_basicblocks(ctx)
diff --git a/vyper/venom/dominators.py b/vyper/venom/dominators.py
new file mode 100644
index 0000000000..b69c17e1d8
--- /dev/null
+++ b/vyper/venom/dominators.py
@@ -0,0 +1,166 @@
+from vyper.exceptions import CompilerPanic
+from vyper.utils import OrderedSet
+from vyper.venom.basicblock import IRBasicBlock
+from vyper.venom.function import IRFunction
+
+
+class DominatorTree:
+ """
+ Dominator tree implementation. This class computes the dominator tree of a
+ function and provides methods to query the tree. The tree is computed using
+ the Lengauer-Tarjan algorithm.
+ """
+
+ ctx: IRFunction
+ entry_block: IRBasicBlock
+ dfs_order: dict[IRBasicBlock, int]
+ dfs_walk: list[IRBasicBlock]
+ dominators: dict[IRBasicBlock, OrderedSet[IRBasicBlock]]
+ immediate_dominators: dict[IRBasicBlock, IRBasicBlock]
+ dominated: dict[IRBasicBlock, OrderedSet[IRBasicBlock]]
+ dominator_frontiers: dict[IRBasicBlock, OrderedSet[IRBasicBlock]]
+
+ @classmethod
+ def build_dominator_tree(cls, ctx, entry):
+ ret = DominatorTree()
+ ret.compute(ctx, entry)
+ return ret
+
+ def compute(self, ctx: IRFunction, entry: IRBasicBlock):
+ """
+ Compute the dominator tree.
+ """
+ self.ctx = ctx
+ self.entry_block = entry
+ self.dfs_order = {}
+ self.dfs_walk = []
+ self.dominators = {}
+ self.immediate_dominators = {}
+ self.dominated = {}
+ self.dominator_frontiers = {}
+
+ self._compute_dfs(self.entry_block, OrderedSet())
+ self._compute_dominators()
+ self._compute_idoms()
+ self._compute_df()
+
+ def dominates(self, bb1, bb2):
+ """
+ Check if bb1 dominates bb2.
+ """
+ return bb2 in self.dominators[bb1]
+
+ def immediate_dominator(self, bb):
+ """
+ Return the immediate dominator of a basic block.
+ """
+ return self.immediate_dominators.get(bb)
+
+ def _compute_dominators(self):
+ """
+ Compute dominators
+ """
+ basic_blocks = list(self.dfs_order.keys())
+ self.dominators = {bb: OrderedSet(basic_blocks) for bb in basic_blocks}
+ self.dominators[self.entry_block] = OrderedSet([self.entry_block])
+ changed = True
+ count = len(basic_blocks) ** 2 # TODO: find a proper bound for this
+ while changed:
+ count -= 1
+ if count < 0:
+ raise CompilerPanic("Dominators computation failed to converge")
+ changed = False
+ for bb in basic_blocks:
+ if bb == self.entry_block:
+ continue
+ preds = bb.cfg_in
+ if len(preds) == 0:
+ continue
+ new_dominators = OrderedSet.intersection(*[self.dominators[pred] for pred in preds])
+ new_dominators.add(bb)
+ if new_dominators != self.dominators[bb]:
+ self.dominators[bb] = new_dominators
+ changed = True
+
+ def _compute_idoms(self):
+ """
+ Compute immediate dominators
+ """
+ self.immediate_dominators = {bb: None for bb in self.dfs_order.keys()}
+ self.immediate_dominators[self.entry_block] = self.entry_block
+ for bb in self.dfs_walk:
+ if bb == self.entry_block:
+ continue
+ doms = sorted(self.dominators[bb], key=lambda x: self.dfs_order[x])
+ self.immediate_dominators[bb] = doms[1]
+
+ self.dominated = {bb: OrderedSet() for bb in self.dfs_walk}
+ for dom, target in self.immediate_dominators.items():
+ self.dominated[target].add(dom)
+
+ def _compute_df(self):
+ """
+ Compute dominance frontier
+ """
+ basic_blocks = self.dfs_walk
+ self.dominator_frontiers = {bb: OrderedSet() for bb in basic_blocks}
+
+ for bb in self.dfs_walk:
+ if len(bb.cfg_in) > 1:
+ for pred in bb.cfg_in:
+ runner = pred
+ while runner != self.immediate_dominators[bb]:
+ self.dominator_frontiers[runner].add(bb)
+ runner = self.immediate_dominators[runner]
+
+ def dominance_frontier(self, basic_blocks: list[IRBasicBlock]) -> OrderedSet[IRBasicBlock]:
+ """
+ Compute dominance frontier of a set of basic blocks.
+ """
+ df = OrderedSet[IRBasicBlock]()
+ for bb in basic_blocks:
+ df.update(self.dominator_frontiers[bb])
+ return df
+
+ def _intersect(self, bb1, bb2):
+ """
+ Find the nearest common dominator of two basic blocks.
+ """
+ dfs_order = self.dfs_order
+ while bb1 != bb2:
+ while dfs_order[bb1] < dfs_order[bb2]:
+ bb1 = self.immediate_dominators[bb1]
+ while dfs_order[bb1] > dfs_order[bb2]:
+ bb2 = self.immediate_dominators[bb2]
+ return bb1
+
+ def _compute_dfs(self, entry: IRBasicBlock, visited):
+ """
+ Depth-first search to compute the DFS order of the basic blocks. This
+ is used to compute the dominator tree. The sequence of basic blocks in
+ the DFS order is stored in `self.dfs_walk`. The DFS order of each basic
+ block is stored in `self.dfs_order`.
+ """
+ visited.add(entry)
+
+ for bb in entry.cfg_out:
+ if bb not in visited:
+ self._compute_dfs(bb, visited)
+
+ self.dfs_walk.append(entry)
+ self.dfs_order[entry] = len(self.dfs_walk)
+
+ def as_graph(self) -> str:
+ """
+ Generate a graphviz representation of the dominator tree.
+ """
+ lines = ["digraph dominator_tree {"]
+ for bb in self.ctx.basic_blocks:
+ if bb == self.entry_block:
+ continue
+ idom = self.immediate_dominator(bb)
+ if idom is None:
+ continue
+ lines.append(f' " {idom.label} " -> " {bb.label} "')
+ lines.append("}")
+ return "\n".join(lines)
diff --git a/vyper/venom/function.py b/vyper/venom/function.py
index 771dcf73ce..d1680385f5 100644
--- a/vyper/venom/function.py
+++ b/vyper/venom/function.py
@@ -1,12 +1,14 @@
-from typing import Optional
+from typing import Iterator, Optional
+from vyper.codegen.ir_node import IRnode
+from vyper.utils import OrderedSet
from vyper.venom.basicblock import (
+ CFG_ALTERING_INSTRUCTIONS,
IRBasicBlock,
IRInstruction,
IRLabel,
IROperand,
IRVariable,
- MemType,
)
GLOBAL_LABEL = IRLabel("__global")
@@ -27,6 +29,11 @@ class IRFunction:
last_label: int
last_variable: int
+ # Used during code generation
+ _ast_source_stack: list[int]
+ _error_msg_stack: list[str]
+ _bb_index: dict[str, int]
+
def __init__(self, name: IRLabel = None) -> None:
if name is None:
name = GLOBAL_LABEL
@@ -40,6 +47,10 @@ def __init__(self, name: IRLabel = None) -> None:
self.last_label = 0
self.last_variable = 0
+ self._ast_source_stack = []
+ self._error_msg_stack = []
+ self._bb_index = {}
+
self.add_entry_point(name)
self.append_basic_block(IRBasicBlock(name, self))
@@ -62,10 +73,22 @@ def append_basic_block(self, bb: IRBasicBlock) -> IRBasicBlock:
assert isinstance(bb, IRBasicBlock), f"append_basic_block takes IRBasicBlock, got '{bb}'"
self.basic_blocks.append(bb)
- # TODO add sanity check somewhere that basic blocks have unique labels
-
return self.basic_blocks[-1]
+ def _get_basicblock_index(self, label: str):
+ # perf: keep an "index" of labels to block indices to
+ # perform fast lookup.
+ # TODO: maybe better just to throw basic blocks in an ordered
+ # dict of some kind.
+ ix = self._bb_index.get(label, -1)
+ if 0 <= ix < len(self.basic_blocks) and self.basic_blocks[ix].label == label:
+ return ix
+ # do a reindex
+ self._bb_index = dict((bb.label.name, ix) for ix, bb in enumerate(self.basic_blocks))
+ # sanity check - no duplicate labels
+ assert len(self._bb_index) == len(self.basic_blocks)
+ return self._bb_index[label]
+
def get_basic_block(self, label: Optional[str] = None) -> IRBasicBlock:
"""
Get basic block by label.
@@ -73,49 +96,97 @@ def get_basic_block(self, label: Optional[str] = None) -> IRBasicBlock:
"""
if label is None:
return self.basic_blocks[-1]
- for bb in self.basic_blocks:
- if bb.label.value == label:
- return bb
- raise AssertionError(f"Basic block '{label}' not found")
+ ix = self._get_basicblock_index(label)
+ return self.basic_blocks[ix]
def get_basic_block_after(self, label: IRLabel) -> IRBasicBlock:
"""
Get basic block after label.
"""
- for i, bb in enumerate(self.basic_blocks[:-1]):
- if bb.label.value == label.value:
- return self.basic_blocks[i + 1]
+ ix = self._get_basicblock_index(label.value)
+ if 0 <= ix < len(self.basic_blocks) - 1:
+ return self.basic_blocks[ix + 1]
raise AssertionError(f"Basic block after '{label}' not found")
+ def get_terminal_basicblocks(self) -> Iterator[IRBasicBlock]:
+ """
+ Get basic blocks that are terminal.
+ """
+ for bb in self.basic_blocks:
+ if bb.is_terminal:
+ yield bb
+
def get_basicblocks_in(self, basic_block: IRBasicBlock) -> list[IRBasicBlock]:
"""
- Get basic blocks that contain label.
+ Get basic blocks that point to the given basic block
"""
return [bb for bb in self.basic_blocks if basic_block.label in bb.cfg_in]
- def get_next_label(self) -> IRLabel:
+ def get_next_label(self, suffix: str = "") -> IRLabel:
+ if suffix != "":
+ suffix = f"_{suffix}"
self.last_label += 1
- return IRLabel(f"{self.last_label}")
+ return IRLabel(f"{self.last_label}{suffix}")
- def get_next_variable(
- self, mem_type: MemType = MemType.OPERAND_STACK, mem_addr: Optional[int] = None
- ) -> IRVariable:
+ def get_next_variable(self) -> IRVariable:
self.last_variable += 1
- return IRVariable(f"%{self.last_variable}", mem_type, mem_addr)
+ return IRVariable(f"%{self.last_variable}")
def get_last_variable(self) -> str:
return f"%{self.last_variable}"
def remove_unreachable_blocks(self) -> int:
- removed = 0
+ self._compute_reachability()
+
+ removed = []
new_basic_blocks = []
+
+ # Remove unreachable basic blocks
for bb in self.basic_blocks:
- if not bb.is_reachable and bb.label not in self.entry_points:
- removed += 1
+ if not bb.is_reachable:
+ removed.append(bb)
else:
new_basic_blocks.append(bb)
self.basic_blocks = new_basic_blocks
- return removed
+
+ # Remove phi instructions that reference removed basic blocks
+ for bb in removed:
+ for out_bb in bb.cfg_out:
+ out_bb.remove_cfg_in(bb)
+ for inst in out_bb.instructions:
+ if inst.opcode != "phi":
+ continue
+ in_labels = inst.get_label_operands()
+ if bb.label in in_labels:
+ out_bb.remove_instruction(inst)
+
+ return len(removed)
+
+ def _compute_reachability(self) -> None:
+ """
+ Compute reachability of basic blocks.
+ """
+ for bb in self.basic_blocks:
+ bb.reachable = OrderedSet()
+ bb.is_reachable = False
+
+ for entry in self.entry_points:
+ entry_bb = self.get_basic_block(entry.value)
+ self._compute_reachability_from(entry_bb)
+
+ def _compute_reachability_from(self, bb: IRBasicBlock) -> None:
+ """
+ Compute reachability of basic blocks from bb.
+ """
+ if bb.is_reachable:
+ return
+ bb.is_reachable = True
+ for inst in bb.instructions:
+ if inst.opcode in CFG_ALTERING_INSTRUCTIONS or inst.opcode == "invoke":
+ for op in inst.get_label_operands():
+ out_bb = self.get_basic_block(op.value)
+ bb.reachable.add(out_bb)
+ self._compute_reachability_from(out_bb)
def append_data(self, opcode: str, args: list[IROperand]) -> None:
"""
@@ -147,6 +218,25 @@ def normalized(self) -> bool:
# The function is normalized
return True
+ def push_source(self, ir):
+ if isinstance(ir, IRnode):
+ self._ast_source_stack.append(ir.ast_source)
+ self._error_msg_stack.append(ir.error_msg)
+
+ def pop_source(self):
+ assert len(self._ast_source_stack) > 0, "Empty source stack"
+ self._ast_source_stack.pop()
+ assert len(self._error_msg_stack) > 0, "Empty error stack"
+ self._error_msg_stack.pop()
+
+ @property
+ def ast_source(self) -> Optional[int]:
+ return self._ast_source_stack[-1] if len(self._ast_source_stack) > 0 else None
+
+ @property
+ def error_msg(self) -> Optional[str]:
+ return self._error_msg_stack[-1] if len(self._error_msg_stack) > 0 else None
+
def copy(self):
new = IRFunction(self.name)
new.basic_blocks = self.basic_blocks.copy()
@@ -155,6 +245,32 @@ def copy(self):
new.last_variable = self.last_variable
return new
+ def as_graph(self) -> str:
+ import html
+
+ def _make_label(bb):
+ ret = '<
'
+ ret += f'{html.escape(str(bb.label))} |
\n'
+ for inst in bb.instructions:
+ ret += f'{html.escape(str(inst))} |
\n'
+ ret += "
>"
+
+ return ret
+ # return f"{bb.label.value}:\n" + "\n".join([f" {inst}" for inst in bb.instructions])
+
+ ret = "digraph G {\n"
+
+ for bb in self.basic_blocks:
+ for out_bb in bb.cfg_out:
+ ret += f' "{bb.label.value}" -> "{out_bb.label.value}"\n'
+
+ for bb in self.basic_blocks:
+ ret += f' "{bb.label.value}" [shape=plaintext, '
+ ret += f'label={_make_label(bb)}, fontname="Courier" fontsize="8"]\n'
+
+ ret += "}\n"
+ return ret
+
def __repr__(self) -> str:
str = f"IRFunction: {self.name}\n"
for bb in self.basic_blocks:
diff --git a/vyper/venom/ir_node_to_venom.py b/vyper/venom/ir_node_to_venom.py
index b3ac3c1ad7..f610e17f58 100644
--- a/vyper/venom/ir_node_to_venom.py
+++ b/vyper/venom/ir_node_to_venom.py
@@ -1,12 +1,10 @@
+import functools
+import re
from typing import Optional
-from vyper.codegen.context import VariableRecord
from vyper.codegen.ir_node import IRnode
from vyper.evm.opcodes import get_opcodes
-from vyper.exceptions import CompilerPanic
-from vyper.ir.compile_ir import is_mem_sym, is_symbol
-from vyper.semantics.types.function import ContractFunctionT
-from vyper.utils import MemoryPositions, OrderedSet
+from vyper.utils import MemoryPositions
from vyper.venom.basicblock import (
IRBasicBlock,
IRInstruction,
@@ -14,12 +12,17 @@
IRLiteral,
IROperand,
IRVariable,
- MemType,
)
from vyper.venom.function import IRFunction
-_BINARY_IR_INSTRUCTIONS = frozenset(
+# Instructions that are mapped to their inverse
+INVERSE_MAPPED_IR_INSTRUCTIONS = {"ne": "eq", "le": "gt", "sle": "sgt", "ge": "lt", "sge": "slt"}
+
+# Instructions that have a direct EVM opcode equivalent and can
+# be passed through to the EVM assembly without special handling
+PASS_THROUGH_INSTRUCTIONS = frozenset(
[
+ # binary instructions
"eq",
"gt",
"lt",
@@ -27,6 +30,7 @@
"sgt",
"shr",
"shl",
+ "sar",
"or",
"xor",
"and",
@@ -34,98 +38,104 @@
"sub",
"mul",
"div",
+ "smul",
+ "sdiv",
"mod",
+ "smod",
"exp",
"sha3",
"sha3_64",
"signextend",
+ "chainid",
+ "basefee",
+ "timestamp",
+ "blockhash",
+ "caller",
+ "selfbalance",
+ "calldatasize",
+ "callvalue",
+ "address",
+ "origin",
+ "codesize",
+ "gas",
+ "gasprice",
+ "gaslimit",
+ "returndatasize",
+ "iload",
+ "sload",
+ "tload",
+ "coinbase",
+ "number",
+ "prevrandao",
+ "difficulty",
+ "iszero",
+ "not",
+ "calldataload",
+ "extcodesize",
+ "extcodehash",
+ "balance",
+ "msize",
+ "basefee",
+ "invalid",
+ "stop",
+ "selfdestruct",
+ "assert",
+ "assert_unreachable",
+ "exit",
+ "calldatacopy",
+ "mcopy",
+ "extcodecopy",
+ "codecopy",
+ "returndatacopy",
+ "revert",
+ "istore",
+ "sstore",
+ "tstore",
+ "create",
+ "create2",
+ "addmod",
+ "mulmod",
+ "call",
+ "delegatecall",
+ "staticcall",
]
)
-# Instructions that are mapped to their inverse
-INVERSE_MAPPED_IR_INSTRUCTIONS = {"ne": "eq", "le": "gt", "sle": "sgt", "ge": "lt", "sge": "slt"}
-
-# Instructions that have a direct EVM opcode equivalent and can
-# be passed through to the EVM assembly without special handling
-PASS_THROUGH_INSTRUCTIONS = [
- "chainid",
- "basefee",
- "timestamp",
- "blockhash",
- "caller",
- "selfbalance",
- "calldatasize",
- "callvalue",
- "address",
- "origin",
- "codesize",
- "gas",
- "gasprice",
- "gaslimit",
- "returndatasize",
- "coinbase",
- "number",
- "iszero",
- "not",
- "calldataload",
- "extcodesize",
- "extcodehash",
- "balance",
-]
+NOOP_INSTRUCTIONS = frozenset(["pass", "cleanup_repeat", "var_list", "unique_symbol"])
SymbolTable = dict[str, Optional[IROperand]]
-def _get_symbols_common(a: dict, b: dict) -> dict:
- ret = {}
- # preserves the ordering in `a`
- for k in a.keys():
- if k not in b:
- continue
- if a[k] == b[k]:
- continue
- ret[k] = a[k], b[k]
- return ret
-
-
# convert IRnode directly to venom
def ir_node_to_venom(ir: IRnode) -> IRFunction:
ctx = IRFunction()
- _convert_ir_bb(ctx, ir, {}, OrderedSet(), {})
+ _convert_ir_bb(ctx, ir, {})
# Patch up basic blocks. Connect unterminated blocks to the next with
# a jump. terminate final basic block with STOP.
for i, bb in enumerate(ctx.basic_blocks):
if not bb.is_terminated:
- if i < len(ctx.basic_blocks) - 1:
- bb.append_instruction("jmp", ctx.basic_blocks[i + 1].label)
+ if len(ctx.basic_blocks) - 1 > i:
+ # TODO: revisit this. When contructor calls internal functions they
+ # are linked to the last ctor block. Should separate them before this
+ # so we don't have to handle this here
+ if ctx.basic_blocks[i + 1].label.value.startswith("internal"):
+ bb.append_instruction("stop")
+ else:
+ bb.append_instruction("jmp", ctx.basic_blocks[i + 1].label)
else:
- bb.append_instruction("stop")
+ bb.append_instruction("exit")
return ctx
-def _convert_binary_op(
- ctx: IRFunction,
- ir: IRnode,
- symbols: SymbolTable,
- variables: OrderedSet,
- allocated_variables: dict[str, IRVariable],
- swap: bool = False,
-) -> Optional[IRVariable]:
- ir_args = ir.args[::-1] if swap else ir.args
- arg_0, arg_1 = _convert_ir_bb_list(ctx, ir_args, symbols, variables, allocated_variables)
-
- assert isinstance(ir.value, str) # mypy hint
- return ctx.get_basic_block().append_instruction(ir.value, arg_1, arg_0)
-
-
def _append_jmp(ctx: IRFunction, label: IRLabel) -> None:
- ctx.get_basic_block().append_instruction("jmp", label)
+ bb = ctx.get_basic_block()
+ if bb.is_terminated:
+ bb = IRBasicBlock(ctx.get_next_label("jmp_target"), ctx)
+ ctx.append_basic_block(bb)
- label = ctx.get_next_label()
- bb = IRBasicBlock(label, ctx)
- ctx.append_basic_block(bb)
+ bb.append_instruction("jmp", label)
def _new_block(ctx: IRFunction) -> IRBasicBlock:
@@ -134,65 +144,46 @@ def _new_block(ctx: IRFunction) -> IRBasicBlock:
return bb
-def _handle_self_call(
- ctx: IRFunction,
- ir: IRnode,
- symbols: SymbolTable,
- variables: OrderedSet,
- allocated_variables: dict[str, IRVariable],
-) -> Optional[IRVariable]:
- func_t = ir.passthrough_metadata.get("func_t", None)
- args_ir = ir.passthrough_metadata["args_ir"]
+def _append_return_args(ctx: IRFunction, ofst: int = 0, size: int = 0):
+ bb = ctx.get_basic_block()
+ if bb.is_terminated:
+ bb = IRBasicBlock(ctx.get_next_label("exit_to"), ctx)
+ ctx.append_basic_block(bb)
+ ret_ofst = IRVariable("ret_ofst")
+ ret_size = IRVariable("ret_size")
+ bb.append_instruction("store", ofst, ret=ret_ofst)
+ bb.append_instruction("store", size, ret=ret_size)
+
+
+def _handle_self_call(ctx: IRFunction, ir: IRnode, symbols: SymbolTable) -> Optional[IRVariable]:
+ setup_ir = ir.args[1]
goto_ir = [ir for ir in ir.args if ir.value == "goto"][0]
target_label = goto_ir.args[0].value # goto
- return_buf = goto_ir.args[1] # return buffer
+ return_buf_ir = goto_ir.args[1] # return buffer
ret_args: list[IROperand] = [IRLabel(target_label)] # type: ignore
- for arg in args_ir:
- if arg.is_literal:
- sym = symbols.get(f"&{arg.value}", None)
- if sym is None:
- ret = _convert_ir_bb(ctx, arg, symbols, variables, allocated_variables)
- ret_args.append(ret)
- else:
- ret_args.append(sym) # type: ignore
- else:
- ret = _convert_ir_bb(ctx, arg._optimized, symbols, variables, allocated_variables)
- if arg.location and arg.location.load_op == "calldataload":
- bb = ctx.get_basic_block()
- ret = bb.append_instruction(arg.location.load_op, ret)
- ret_args.append(ret)
+ if setup_ir != goto_ir:
+ _convert_ir_bb(ctx, setup_ir, symbols)
- if return_buf.is_literal:
- ret_args.append(return_buf.value) # type: ignore
+ return_buf = _convert_ir_bb(ctx, return_buf_ir, symbols)
bb = ctx.get_basic_block()
- do_ret = func_t.return_type is not None
- if do_ret:
- invoke_ret = bb.append_invoke_instruction(ret_args, returns=True) # type: ignore
- allocated_variables["return_buffer"] = invoke_ret # type: ignore
- return invoke_ret
- else:
- bb.append_invoke_instruction(ret_args, returns=False) # type: ignore
- return None
+ if len(goto_ir.args) > 2:
+ ret_args.append(return_buf) # type: ignore
+
+ bb.append_invoke_instruction(ret_args, returns=False) # type: ignore
+
+ return return_buf
def _handle_internal_func(
- ctx: IRFunction, ir: IRnode, func_t: ContractFunctionT, symbols: SymbolTable
-) -> IRnode:
+ ctx: IRFunction, ir: IRnode, does_return_data: bool, symbols: SymbolTable
+):
bb = IRBasicBlock(IRLabel(ir.args[0].args[0].value, True), ctx) # type: ignore
bb = ctx.append_basic_block(bb)
- old_ir_mempos = 0
- old_ir_mempos += 64
-
- for arg in func_t.arguments:
- symbols[f"&{old_ir_mempos}"] = bb.append_instruction("param")
- bb.instructions[-1].annotation = arg.name
- old_ir_mempos += 32 # arg.typ.memory_bytes_required
-
# return buffer
- if func_t.return_type is not None:
+ if does_return_data:
symbols["return_buffer"] = bb.append_instruction("param")
bb.instructions[-1].annotation = "return_buffer"
@@ -200,17 +191,16 @@ def _handle_internal_func(
symbols["return_pc"] = bb.append_instruction("param")
bb.instructions[-1].annotation = "return_pc"
- return ir.args[0].args[2]
+ _convert_ir_bb(ctx, ir.args[0].args[2], symbols)
def _convert_ir_simple_node(
- ctx: IRFunction,
- ir: IRnode,
- symbols: SymbolTable,
- variables: OrderedSet,
- allocated_variables: dict[str, IRVariable],
+ ctx: IRFunction, ir: IRnode, symbols: SymbolTable
) -> Optional[IRVariable]:
- args = [_convert_ir_bb(ctx, arg, symbols, variables, allocated_variables) for arg in ir.args]
+ # execute in order
+ args = _convert_ir_bb_list(ctx, ir.args, symbols)
+ # reverse output variables for stack
+ args.reverse()
return ctx.get_basic_block().append_instruction(ir.value, *args) # type: ignore
@@ -218,265 +208,162 @@ def _convert_ir_simple_node(
_continue_target: Optional[IRBasicBlock] = None
-def _get_variable_from_address(
- variables: OrderedSet[VariableRecord], addr: int
-) -> Optional[VariableRecord]:
- assert isinstance(addr, int), "non-int address"
- for var in variables.keys():
- if var.location.name != "memory":
- continue
- if addr >= var.pos and addr < var.pos + var.size: # type: ignore
- return var
- return None
-
-
-def _append_return_for_stack_operand(
- ctx: IRFunction, symbols: SymbolTable, ret_ir: IRVariable, last_ir: IRVariable
-) -> None:
- bb = ctx.get_basic_block()
- if isinstance(ret_ir, IRLiteral):
- sym = symbols.get(f"&{ret_ir.value}", None)
- new_var = bb.append_instruction("alloca", 32, ret_ir)
- bb.append_instruction("mstore", sym, new_var) # type: ignore
- else:
- sym = symbols.get(ret_ir.value, None)
- if sym is None:
- # FIXME: needs real allocations
- new_var = bb.append_instruction("alloca", 32, 0)
- bb.append_instruction("mstore", ret_ir, new_var) # type: ignore
- else:
- new_var = ret_ir
- bb.append_instruction("return", last_ir, new_var) # type: ignore
-
-
-def _convert_ir_bb_list(ctx, ir, symbols, variables, allocated_variables):
+def _convert_ir_bb_list(ctx, ir, symbols):
ret = []
for ir_node in ir:
- venom = _convert_ir_bb(ctx, ir_node, symbols, variables, allocated_variables)
- assert venom is not None, ir_node
+ venom = _convert_ir_bb(ctx, ir_node, symbols)
ret.append(venom)
return ret
-def _convert_ir_bb(ctx, ir, symbols, variables, allocated_variables):
- assert isinstance(ir, IRnode), ir
- assert isinstance(variables, OrderedSet)
- global _break_target, _continue_target
+current_func = None
+var_list: list[str] = []
- frame_info = ir.passthrough_metadata.get("frame_info", None)
- if frame_info is not None:
- local_vars = OrderedSet[VariableRecord](frame_info.frame_vars.values())
- variables |= local_vars
- assert isinstance(variables, OrderedSet)
+def pop_source_on_return(func):
+ @functools.wraps(func)
+ def pop_source(*args, **kwargs):
+ ctx = args[0]
+ ret = func(*args, **kwargs)
+ ctx.pop_source()
+ return ret
- if ir.value in _BINARY_IR_INSTRUCTIONS:
- return _convert_binary_op(
- ctx, ir, symbols, variables, allocated_variables, ir.value in ["sha3_64"]
- )
+ return pop_source
+
+
+@pop_source_on_return
+def _convert_ir_bb(ctx, ir, symbols):
+ assert isinstance(ir, IRnode), ir
+ global _break_target, _continue_target, current_func, var_list
+
+ ctx.push_source(ir)
- elif ir.value in INVERSE_MAPPED_IR_INSTRUCTIONS:
+ if ir.value in INVERSE_MAPPED_IR_INSTRUCTIONS:
org_value = ir.value
ir.value = INVERSE_MAPPED_IR_INSTRUCTIONS[ir.value]
- new_var = _convert_binary_op(ctx, ir, symbols, variables, allocated_variables)
+ new_var = _convert_ir_simple_node(ctx, ir, symbols)
ir.value = org_value
return ctx.get_basic_block().append_instruction("iszero", new_var)
-
elif ir.value in PASS_THROUGH_INSTRUCTIONS:
- return _convert_ir_simple_node(ctx, ir, symbols, variables, allocated_variables)
-
- elif ir.value in ["pass", "stop", "return"]:
- pass
+ return _convert_ir_simple_node(ctx, ir, symbols)
+ elif ir.value == "return":
+ ctx.get_basic_block().append_instruction(
+ "return", IRVariable("ret_size"), IRVariable("ret_ofst")
+ )
elif ir.value == "deploy":
ctx.ctor_mem_size = ir.args[0].value
ctx.immutables_len = ir.args[2].value
return None
elif ir.value == "seq":
- func_t = ir.passthrough_metadata.get("func_t", None)
+ if len(ir.args) == 0:
+ return None
if ir.is_self_call:
- return _handle_self_call(ctx, ir, symbols, variables, allocated_variables)
- elif func_t is not None:
- symbols = {}
- allocated_variables = {}
- variables = OrderedSet(
- {v: True for v in ir.passthrough_metadata["frame_info"].frame_vars.values()}
- )
- if func_t.is_internal:
- ir = _handle_internal_func(ctx, ir, func_t, symbols)
- # fallthrough
-
- ret = None
- for ir_node in ir.args: # NOTE: skip the last one
- ret = _convert_ir_bb(ctx, ir_node, symbols, variables, allocated_variables)
-
- return ret
- elif ir.value in ["staticcall", "call"]: # external call
- idx = 0
- gas = _convert_ir_bb(ctx, ir.args[idx], symbols, variables, allocated_variables)
- address = _convert_ir_bb(ctx, ir.args[idx + 1], symbols, variables, allocated_variables)
-
- value = None
- if ir.value == "call":
- value = _convert_ir_bb(ctx, ir.args[idx + 2], symbols, variables, allocated_variables)
- else:
- idx -= 1
-
- argsOffset, argsSize, retOffset, retSize = _convert_ir_bb_list(
- ctx, ir.args[idx + 3 : idx + 7], symbols, variables, allocated_variables
- )
-
- if isinstance(argsOffset, IRLiteral):
- offset = int(argsOffset.value)
- addr = offset - 32 + 4 if offset > 0 else 0
- argsOffsetVar = symbols.get(f"&{addr}", None)
- if argsOffsetVar is None:
- argsOffsetVar = argsOffset
- elif isinstance(argsOffsetVar, IRVariable):
- argsOffsetVar.mem_type = MemType.MEMORY
- argsOffsetVar.mem_addr = addr
- argsOffsetVar.offset = 32 - 4 if offset > 0 else 0
- else: # pragma: nocover
- raise CompilerPanic("unreachable")
+ return _handle_self_call(ctx, ir, symbols)
+ elif ir.args[0].value == "label":
+ current_func = ir.args[0].args[0].value
+ is_external = current_func.startswith("external")
+ is_internal = current_func.startswith("internal")
+ if is_internal or len(re.findall(r"external.*__init__\(.*_deploy", current_func)) > 0:
+ # Internal definition
+ var_list = ir.args[0].args[1]
+ does_return_data = IRnode.from_list(["return_buffer"]) in var_list.args
+ symbols = {}
+ _handle_internal_func(ctx, ir, does_return_data, symbols)
+ for ir_node in ir.args[1:]:
+ ret = _convert_ir_bb(ctx, ir_node, symbols)
+
+ return ret
+ elif is_external:
+ ret = _convert_ir_bb(ctx, ir.args[0], symbols)
+ _append_return_args(ctx)
else:
- argsOffsetVar = argsOffset
-
- retOffsetValue = int(retOffset.value) if retOffset else 0
- retVar = ctx.get_next_variable(MemType.MEMORY, retOffsetValue)
- symbols[f"&{retOffsetValue}"] = retVar
+ bb = ctx.get_basic_block()
+ if bb.is_terminated:
+ bb = IRBasicBlock(ctx.get_next_label("seq"), ctx)
+ ctx.append_basic_block(bb)
+ ret = _convert_ir_bb(ctx, ir.args[0], symbols)
- bb = ctx.get_basic_block()
+ for ir_node in ir.args[1:]:
+ ret = _convert_ir_bb(ctx, ir_node, symbols)
- if ir.value == "call":
- args = [retSize, retOffset, argsSize, argsOffsetVar, value, address, gas]
- return bb.append_instruction(ir.value, *args)
- else:
- args = [retSize, retOffset, argsSize, argsOffsetVar, address, gas]
- return bb.append_instruction(ir.value, *args)
+ return ret
elif ir.value == "if":
cond = ir.args[0]
# convert the condition
- cont_ret = _convert_ir_bb(ctx, cond, symbols, variables, allocated_variables)
- current_bb = ctx.get_basic_block()
+ cont_ret = _convert_ir_bb(ctx, cond, symbols)
+ cond_block = ctx.get_basic_block()
+
+ cond_symbols = symbols.copy()
- else_block = IRBasicBlock(ctx.get_next_label(), ctx)
+ else_block = IRBasicBlock(ctx.get_next_label("else"), ctx)
ctx.append_basic_block(else_block)
# convert "else"
else_ret_val = None
- else_syms = symbols.copy()
if len(ir.args) == 3:
- else_ret_val = _convert_ir_bb(
- ctx, ir.args[2], else_syms, variables, allocated_variables.copy()
- )
+ else_ret_val = _convert_ir_bb(ctx, ir.args[2], cond_symbols)
if isinstance(else_ret_val, IRLiteral):
assert isinstance(else_ret_val.value, int) # help mypy
else_ret_val = ctx.get_basic_block().append_instruction("store", else_ret_val)
- after_else_syms = else_syms.copy()
- else_block = ctx.get_basic_block()
+
+ else_block_finish = ctx.get_basic_block()
# convert "then"
- then_block = IRBasicBlock(ctx.get_next_label(), ctx)
+ cond_symbols = symbols.copy()
+
+ then_block = IRBasicBlock(ctx.get_next_label("then"), ctx)
ctx.append_basic_block(then_block)
- then_ret_val = _convert_ir_bb(ctx, ir.args[1], symbols, variables, allocated_variables)
+ then_ret_val = _convert_ir_bb(ctx, ir.args[1], cond_symbols)
if isinstance(then_ret_val, IRLiteral):
then_ret_val = ctx.get_basic_block().append_instruction("store", then_ret_val)
- current_bb.append_instruction("jnz", cont_ret, then_block.label, else_block.label)
+ cond_block.append_instruction("jnz", cont_ret, then_block.label, else_block.label)
- after_then_syms = symbols.copy()
- then_block = ctx.get_basic_block()
+ then_block_finish = ctx.get_basic_block()
# exit bb
- exit_label = ctx.get_next_label()
- exit_bb = IRBasicBlock(exit_label, ctx)
+ exit_bb = IRBasicBlock(ctx.get_next_label("if_exit"), ctx)
exit_bb = ctx.append_basic_block(exit_bb)
- if_ret = None
+ if_ret = ctx.get_next_variable()
if then_ret_val is not None and else_ret_val is not None:
- if_ret = exit_bb.append_instruction(
- "phi", then_block.label, then_ret_val, else_block.label, else_ret_val
- )
-
- common_symbols = _get_symbols_common(after_then_syms, after_else_syms)
- for sym, val in common_symbols.items():
- ret = exit_bb.append_instruction(
- "phi", then_block.label, val[0], else_block.label, val[1]
- )
- old_var = symbols.get(sym, None)
- symbols[sym] = ret
- if old_var is not None:
- for idx, var_rec in allocated_variables.items(): # type: ignore
- if var_rec.value == old_var.value:
- allocated_variables[idx] = ret # type: ignore
-
- if not else_block.is_terminated:
- else_block.append_instruction("jmp", exit_bb.label)
-
- if not then_block.is_terminated:
- then_block.append_instruction("jmp", exit_bb.label)
+ then_block_finish.append_instruction("store", then_ret_val, ret=if_ret)
+ else_block_finish.append_instruction("store", else_ret_val, ret=if_ret)
+
+ if not else_block_finish.is_terminated:
+ else_block_finish.append_instruction("jmp", exit_bb.label)
+
+ if not then_block_finish.is_terminated:
+ then_block_finish.append_instruction("jmp", exit_bb.label)
return if_ret
elif ir.value == "with":
- ret = _convert_ir_bb(
- ctx, ir.args[1], symbols, variables, allocated_variables
- ) # initialization
+ ret = _convert_ir_bb(ctx, ir.args[1], symbols) # initialization
+
+ ret = ctx.get_basic_block().append_instruction("store", ret)
# Handle with nesting with same symbol
with_symbols = symbols.copy()
sym = ir.args[0]
- if isinstance(ret, IRLiteral):
- new_var = ctx.get_basic_block().append_instruction("store", ret) # type: ignore
- with_symbols[sym.value] = new_var
- else:
- with_symbols[sym.value] = ret # type: ignore
+ with_symbols[sym.value] = ret
- return _convert_ir_bb(ctx, ir.args[2], with_symbols, variables, allocated_variables) # body
+ return _convert_ir_bb(ctx, ir.args[2], with_symbols) # body
elif ir.value == "goto":
_append_jmp(ctx, IRLabel(ir.args[0].value))
elif ir.value == "djump":
- args = [_convert_ir_bb(ctx, ir.args[0], symbols, variables, allocated_variables)]
+ args = [_convert_ir_bb(ctx, ir.args[0], symbols)]
for target in ir.args[1:]:
args.append(IRLabel(target.value))
ctx.get_basic_block().append_instruction("djmp", *args)
_new_block(ctx)
elif ir.value == "set":
sym = ir.args[0]
- arg_1 = _convert_ir_bb(ctx, ir.args[1], symbols, variables, allocated_variables)
- new_var = ctx.get_basic_block().append_instruction("store", arg_1) # type: ignore
- symbols[sym.value] = new_var
-
- elif ir.value == "calldatacopy":
- arg_0, arg_1, size = _convert_ir_bb_list(
- ctx, ir.args, symbols, variables, allocated_variables
- )
-
- new_v = arg_0
- var = (
- _get_variable_from_address(variables, int(arg_0.value))
- if isinstance(arg_0, IRLiteral)
- else None
- )
- bb = ctx.get_basic_block()
- if var is not None:
- if allocated_variables.get(var.name, None) is None:
- new_v = bb.append_instruction("alloca", var.size, var.pos) # type: ignore
- allocated_variables[var.name] = new_v # type: ignore
- bb.append_instruction("calldatacopy", size, arg_1, new_v) # type: ignore
- symbols[f"&{var.pos}"] = new_v # type: ignore
- else:
- bb.append_instruction("calldatacopy", size, arg_1, new_v) # type: ignore
-
- return new_v
- elif ir.value == "codecopy":
- arg_0, arg_1, size = _convert_ir_bb_list(
- ctx, ir.args, symbols, variables, allocated_variables
- )
-
- ctx.get_basic_block().append_instruction("codecopy", size, arg_1, arg_0) # type: ignore
+ arg_1 = _convert_ir_bb(ctx, ir.args[1], symbols)
+ ctx.get_basic_block().append_instruction("store", arg_1, ret=symbols[sym.value])
elif ir.value == "symbol":
return IRLabel(ir.args[0].value, True)
elif ir.value == "data":
@@ -486,15 +373,11 @@ def _convert_ir_bb(ctx, ir, symbols, variables, allocated_variables):
if isinstance(c, int):
assert 0 <= c <= 255, "data with invalid size"
ctx.append_data("db", [c]) # type: ignore
- elif isinstance(c, bytes):
- ctx.append_data("db", [c]) # type: ignore
+ elif isinstance(c.value, bytes):
+ ctx.append_data("db", [c.value]) # type: ignore
elif isinstance(c, IRnode):
- data = _convert_ir_bb(ctx, c, symbols, variables, allocated_variables)
+ data = _convert_ir_bb(ctx, c, symbols)
ctx.append_data("db", [data]) # type: ignore
- elif ir.value == "assert":
- arg_0 = _convert_ir_bb(ctx, ir.args[0], symbols, variables, allocated_variables)
- current_bb = ctx.get_basic_block()
- current_bb.append_instruction("assert", arg_0)
elif ir.value == "label":
label = IRLabel(ir.args[0].value, True)
bb = ctx.get_basic_block()
@@ -502,97 +385,30 @@ def _convert_ir_bb(ctx, ir, symbols, variables, allocated_variables):
bb.append_instruction("jmp", label)
bb = IRBasicBlock(label, ctx)
ctx.append_basic_block(bb)
- _convert_ir_bb(ctx, ir.args[2], symbols, variables, allocated_variables)
+ code = ir.args[2]
+ if code.value == "pass":
+ bb.append_instruction("exit")
+ else:
+ _convert_ir_bb(ctx, code, symbols)
elif ir.value == "exit_to":
- func_t = ir.passthrough_metadata.get("func_t", None)
- assert func_t is not None, "exit_to without func_t"
-
- if func_t.is_external:
- # Hardcoded constructor special case
- bb = ctx.get_basic_block()
- if func_t.name == "__init__":
- label = IRLabel(ir.args[0].value, True)
- bb.append_instruction("jmp", label)
- return None
- if func_t.return_type is None:
- bb.append_instruction("stop")
- return None
- else:
- last_ir = None
- ret_var = ir.args[1]
- deleted = None
- if ret_var.is_literal and symbols.get(f"&{ret_var.value}", None) is not None:
- deleted = symbols[f"&{ret_var.value}"]
- del symbols[f"&{ret_var.value}"]
- for arg in ir.args[2:]:
- last_ir = _convert_ir_bb(ctx, arg, symbols, variables, allocated_variables)
- if deleted is not None:
- symbols[f"&{ret_var.value}"] = deleted
-
- ret_ir = _convert_ir_bb(ctx, ret_var, symbols, variables, allocated_variables)
-
- bb = ctx.get_basic_block()
-
- var = (
- _get_variable_from_address(variables, int(ret_ir.value))
- if isinstance(ret_ir, IRLiteral)
- else None
- )
- if var is not None:
- allocated_var = allocated_variables.get(var.name, None)
- assert allocated_var is not None, "unallocated variable"
- new_var = symbols.get(f"&{ret_ir.value}", allocated_var) # type: ignore
-
- if var.size and int(var.size) > 32:
- offset = int(ret_ir.value) - var.pos # type: ignore
- if offset > 0:
- ptr_var = bb.append_instruction("add", var.pos, offset)
- else:
- ptr_var = allocated_var
- bb.append_instruction("return", last_ir, ptr_var)
- else:
- _append_return_for_stack_operand(ctx, symbols, new_var, last_ir)
- else:
- if isinstance(ret_ir, IRLiteral):
- sym = symbols.get(f"&{ret_ir.value}", None)
- if sym is None:
- bb.append_instruction("return", last_ir, ret_ir)
- else:
- if func_t.return_type.memory_bytes_required > 32:
- new_var = bb.append_instruction("alloca", 32, ret_ir)
- bb.append_instruction("mstore", sym, new_var)
- bb.append_instruction("return", last_ir, new_var)
- else:
- bb.append_instruction("return", last_ir, ret_ir)
- else:
- if last_ir and int(last_ir.value) > 32:
- bb.append_instruction("return", last_ir, ret_ir)
- else:
- ret_buf = 128 # TODO: need allocator
- new_var = bb.append_instruction("alloca", 32, ret_buf)
- bb.append_instruction("mstore", ret_ir, new_var)
- bb.append_instruction("return", last_ir, new_var)
-
- ctx.append_basic_block(IRBasicBlock(ctx.get_next_label(), ctx))
-
+ args = _convert_ir_bb_list(ctx, ir.args[1:], symbols)
+ var_list = args
+ _append_return_args(ctx, *var_list)
+ bb = ctx.get_basic_block()
+ if bb.is_terminated:
+ bb = IRBasicBlock(ctx.get_next_label("exit_to"), ctx)
+ ctx.append_basic_block(bb)
bb = ctx.get_basic_block()
- if func_t.is_internal:
- assert ir.args[1].value == "return_pc", "return_pc not found"
- if func_t.return_type is None:
- bb.append_instruction("ret", symbols["return_pc"])
- else:
- if func_t.return_type.memory_bytes_required > 32:
- bb.append_instruction("ret", symbols["return_buffer"], symbols["return_pc"])
- else:
- ret_by_value = bb.append_instruction("mload", symbols["return_buffer"])
- bb.append_instruction("ret", ret_by_value, symbols["return_pc"])
- elif ir.value == "revert":
- arg_0, arg_1 = _convert_ir_bb_list(ctx, ir.args, symbols, variables, allocated_variables)
- ctx.get_basic_block().append_instruction("revert", arg_1, arg_0)
+ label = IRLabel(ir.args[0].value)
+ if label.value == "return_pc":
+ label = symbols.get("return_pc")
+ bb.append_instruction("ret", label)
+ else:
+ bb.append_instruction("jmp", label)
elif ir.value == "dload":
- arg_0 = _convert_ir_bb(ctx, ir.args[0], symbols, variables, allocated_variables)
+ arg_0 = _convert_ir_bb(ctx, ir.args[0], symbols)
bb = ctx.get_basic_block()
src = bb.append_instruction("add", arg_0, IRLabel("code_end"))
@@ -600,9 +416,7 @@ def _convert_ir_bb(ctx, ir, symbols, variables, allocated_variables):
return bb.append_instruction("mload", MemoryPositions.FREE_VAR_SPACE)
elif ir.value == "dloadbytes":
- dst, src_offset, len_ = _convert_ir_bb_list(
- ctx, ir.args, symbols, variables, allocated_variables
- )
+ dst, src_offset, len_ = _convert_ir_bb_list(ctx, ir.args, symbols)
bb = ctx.get_basic_block()
src = bb.append_instruction("add", src_offset, IRLabel("code_end"))
@@ -610,210 +424,106 @@ def _convert_ir_bb(ctx, ir, symbols, variables, allocated_variables):
return None
elif ir.value == "mload":
- sym_ir = ir.args[0]
- var = (
- _get_variable_from_address(variables, int(sym_ir.value)) if sym_ir.is_literal else None
- )
+ arg_0 = _convert_ir_bb(ctx, ir.args[0], symbols)
bb = ctx.get_basic_block()
- if var is not None:
- if var.size and var.size > 32:
- if allocated_variables.get(var.name, None) is None:
- allocated_variables[var.name] = bb.append_instruction(
- "alloca", var.size, var.pos
- )
-
- offset = int(sym_ir.value) - var.pos
- if offset > 0:
- ptr_var = bb.append_instruction("add", var.pos, offset)
- else:
- ptr_var = allocated_variables[var.name]
+ if isinstance(arg_0, IRVariable):
+ return bb.append_instruction("mload", arg_0)
- return bb.append_instruction("mload", ptr_var)
- else:
- if sym_ir.is_literal:
- sym = symbols.get(f"&{sym_ir.value}", None)
- if sym is None:
- new_var = _convert_ir_bb(
- ctx, sym_ir, symbols, variables, allocated_variables
- )
- symbols[f"&{sym_ir.value}"] = new_var
- if allocated_variables.get(var.name, None) is None:
- allocated_variables[var.name] = new_var
- return new_var
- else:
- return sym
-
- sym = symbols.get(f"&{sym_ir.value}", None)
- assert sym is not None, "unallocated variable"
- return sym
- else:
- if sym_ir.is_literal:
- new_var = symbols.get(f"&{sym_ir.value}", None)
- if new_var is not None:
- return bb.append_instruction("mload", new_var)
- else:
- return bb.append_instruction("mload", sym_ir.value)
- else:
- new_var = _convert_ir_bb(ctx, sym_ir, symbols, variables, allocated_variables)
- #
- # Old IR gets it's return value as a reference in the stack
- # New IR gets it's return value in stack in case of 32 bytes or less
- # So here we detect ahead of time if this mload leads a self call and
- # and we skip the mload
- #
- if sym_ir.is_self_call:
- return new_var
- return bb.append_instruction("mload", new_var)
+ if isinstance(arg_0, IRLiteral):
+ avar = symbols.get(f"%{arg_0.value}")
+ if avar is not None:
+ return bb.append_instruction("mload", avar)
+ return bb.append_instruction("mload", arg_0)
elif ir.value == "mstore":
- sym_ir, arg_1 = _convert_ir_bb_list(ctx, ir.args, symbols, variables, allocated_variables)
-
- bb = ctx.get_basic_block()
-
- var = None
- if isinstance(sym_ir, IRLiteral):
- var = _get_variable_from_address(variables, int(sym_ir.value))
+ # some upstream code depends on reversed order of evaluation --
+ # to fix upstream.
+ arg_1, arg_0 = _convert_ir_bb_list(ctx, reversed(ir.args), symbols)
- if var is not None and var.size is not None:
- if var.size and var.size > 32:
- if allocated_variables.get(var.name, None) is None:
- allocated_variables[var.name] = bb.append_instruction(
- "alloca", var.size, var.pos
- )
-
- offset = int(sym_ir.value) - var.pos
- if offset > 0:
- ptr_var = bb.append_instruction("add", var.pos, offset)
- else:
- ptr_var = allocated_variables[var.name]
+ if isinstance(arg_1, IRVariable):
+ symbols[f"&{arg_0.value}"] = arg_1
- bb.append_instruction("mstore", arg_1, ptr_var)
- else:
- if isinstance(sym_ir, IRLiteral):
- new_var = bb.append_instruction("store", arg_1)
- symbols[f"&{sym_ir.value}"] = new_var
- # if allocated_variables.get(var.name, None) is None:
- allocated_variables[var.name] = new_var
- return new_var
- else:
- if not isinstance(sym_ir, IRLiteral):
- bb.append_instruction("mstore", arg_1, sym_ir)
- return None
-
- sym = symbols.get(f"&{sym_ir.value}", None)
- if sym is None:
- bb.append_instruction("mstore", arg_1, sym_ir)
- if arg_1 and not isinstance(sym_ir, IRLiteral):
- symbols[f"&{sym_ir.value}"] = arg_1
- return None
-
- if isinstance(sym_ir, IRLiteral):
- bb.append_instruction("mstore", arg_1, sym)
- return None
- else:
- symbols[sym_ir.value] = arg_1
- return arg_1
+ ctx.get_basic_block().append_instruction("mstore", arg_1, arg_0)
elif ir.value == "ceil32":
x = ir.args[0]
expanded = IRnode.from_list(["and", ["add", x, 31], ["not", 31]])
- return _convert_ir_bb(ctx, expanded, symbols, variables, allocated_variables)
+ return _convert_ir_bb(ctx, expanded, symbols)
elif ir.value == "select":
- # b ^ ((a ^ b) * cond) where cond is 1 or 0
cond, a, b = ir.args
- expanded = IRnode.from_list(["xor", b, ["mul", cond, ["xor", a, b]]])
- return _convert_ir_bb(ctx, expanded, symbols, variables, allocated_variables)
-
- elif ir.value in ["sload", "iload"]:
- arg_0 = _convert_ir_bb(ctx, ir.args[0], symbols, variables, allocated_variables)
- return ctx.get_basic_block().append_instruction(ir.value, arg_0)
- elif ir.value in ["sstore", "istore"]:
- arg_0, arg_1 = _convert_ir_bb_list(ctx, ir.args, symbols, variables, allocated_variables)
- ctx.get_basic_block().append_instruction(ir.value, arg_1, arg_0)
- elif ir.value == "unique_symbol":
- sym = ir.args[0]
- new_var = ctx.get_next_variable()
- symbols[f"&{sym.value}"] = new_var
- return new_var
+ expanded = IRnode.from_list(
+ [
+ "with",
+ "cond",
+ cond,
+ [
+ "with",
+ "a",
+ a,
+ ["with", "b", b, ["xor", "b", ["mul", "cond", ["xor", "a", "b"]]]],
+ ],
+ ]
+ )
+ return _convert_ir_bb(ctx, expanded, symbols)
elif ir.value == "repeat":
- #
- # repeat(sym, start, end, bound, body)
- # 1) entry block ]
- # 2) init counter block ] -> same block
- # 3) condition block (exit block, body block)
- # 4) body block
- # 5) increment block
- # 6) exit block
- # TODO: Add the extra bounds check after clarify
+
def emit_body_blocks():
global _break_target, _continue_target
old_targets = _break_target, _continue_target
- _break_target, _continue_target = exit_block, increment_block
- _convert_ir_bb(ctx, body, symbols, variables, allocated_variables)
+ _break_target, _continue_target = exit_block, incr_block
+ _convert_ir_bb(ctx, body, symbols.copy())
_break_target, _continue_target = old_targets
sym = ir.args[0]
- start, end, _ = _convert_ir_bb_list(
- ctx, ir.args[1:4], symbols, variables, allocated_variables
- )
+ start, end, _ = _convert_ir_bb_list(ctx, ir.args[1:4], symbols)
+
+ assert ir.args[3].is_literal, "repeat bound expected to be literal"
+
+ bound = ir.args[3].value
+ if (
+ isinstance(end, IRLiteral)
+ and isinstance(start, IRLiteral)
+ and end.value + start.value <= bound
+ ):
+ bound = None
body = ir.args[4]
- entry_block = ctx.get_basic_block()
- cond_block = IRBasicBlock(ctx.get_next_label(), ctx)
- body_block = IRBasicBlock(ctx.get_next_label(), ctx)
- jump_up_block = IRBasicBlock(ctx.get_next_label(), ctx)
- increment_block = IRBasicBlock(ctx.get_next_label(), ctx)
- exit_block = IRBasicBlock(ctx.get_next_label(), ctx)
+ entry_block = IRBasicBlock(ctx.get_next_label("repeat"), ctx)
+ cond_block = IRBasicBlock(ctx.get_next_label("condition"), ctx)
+ body_block = IRBasicBlock(ctx.get_next_label("body"), ctx)
+ incr_block = IRBasicBlock(ctx.get_next_label("incr"), ctx)
+ exit_block = IRBasicBlock(ctx.get_next_label("exit"), ctx)
- counter_inc_var = ctx.get_next_variable()
+ bb = ctx.get_basic_block()
+ bb.append_instruction("jmp", entry_block.label)
+ ctx.append_basic_block(entry_block)
- counter_var = ctx.get_basic_block().append_instruction("store", start)
+ counter_var = entry_block.append_instruction("store", start)
symbols[sym.value] = counter_var
- ctx.get_basic_block().append_instruction("jmp", cond_block.label)
-
- ret = cond_block.append_instruction(
- "phi", entry_block.label, counter_var, increment_block.label, counter_inc_var
- )
- symbols[sym.value] = ret
+ end = entry_block.append_instruction("add", start, end)
+ if bound:
+ bound = entry_block.append_instruction("add", start, bound)
+ entry_block.append_instruction("jmp", cond_block.label)
- xor_ret = cond_block.append_instruction("xor", ret, end)
+ xor_ret = cond_block.append_instruction("xor", counter_var, end)
cont_ret = cond_block.append_instruction("iszero", xor_ret)
ctx.append_basic_block(cond_block)
- start_syms = symbols.copy()
ctx.append_basic_block(body_block)
- emit_body_blocks()
- end_syms = symbols.copy()
- diff_syms = _get_symbols_common(start_syms, end_syms)
-
- replacements = {}
- for sym, val in diff_syms.items():
- new_var = ctx.get_next_variable()
- symbols[sym] = new_var
- replacements[val[0]] = new_var
- replacements[val[1]] = new_var
- cond_block.insert_instruction(
- IRInstruction(
- "phi", [entry_block.label, val[0], increment_block.label, val[1]], new_var
- ),
- 1,
- )
-
- body_block.replace_operands(replacements)
+ if bound:
+ xor_ret = body_block.append_instruction("xor", counter_var, bound)
+ body_block.append_instruction("assert", xor_ret)
+ emit_body_blocks()
body_end = ctx.get_basic_block()
- if not body_end.is_terminated:
- body_end.append_instruction("jmp", jump_up_block.label)
+ if body_end.is_terminated is False:
+ body_end.append_instruction("jmp", incr_block.label)
- jump_up_block.append_instruction("jmp", increment_block.label)
- ctx.append_basic_block(jump_up_block)
-
- increment_block.insert_instruction(
- IRInstruction("add", [ret, IRLiteral(1)], counter_inc_var), 0
+ ctx.append_basic_block(incr_block)
+ incr_block.insert_instruction(
+ IRInstruction("add", [counter_var, IRLiteral(1)], counter_var)
)
-
- increment_block.append_instruction("jmp", cond_block.label)
- ctx.append_basic_block(increment_block)
+ incr_block.append_instruction("jmp", cond_block.label)
ctx.append_basic_block(exit_block)
@@ -826,32 +536,15 @@ def emit_body_blocks():
assert _continue_target is not None, "Continue with no contrinue target"
ctx.get_basic_block().append_instruction("jmp", _continue_target.label)
ctx.append_basic_block(IRBasicBlock(ctx.get_next_label(), ctx))
- elif ir.value == "gas":
- return ctx.get_basic_block().append_instruction("gas")
- elif ir.value == "returndatasize":
- return ctx.get_basic_block().append_instruction("returndatasize")
- elif ir.value == "returndatacopy":
- assert len(ir.args) == 3, "returndatacopy with wrong number of arguments"
- arg_0, arg_1, size = _convert_ir_bb_list(
- ctx, ir.args, symbols, variables, allocated_variables
- )
-
- new_var = ctx.get_basic_block().append_instruction("returndatacopy", arg_1, size)
-
- symbols[f"&{arg_0.value}"] = new_var
- return new_var
- elif ir.value == "selfdestruct":
- arg_0 = _convert_ir_bb(ctx, ir.args[0], symbols, variables, allocated_variables)
- ctx.get_basic_block().append_instruction("selfdestruct", arg_0)
+ elif ir.value in NOOP_INSTRUCTIONS:
+ pass
elif isinstance(ir.value, str) and ir.value.startswith("log"):
- args = reversed(
- [_convert_ir_bb(ctx, arg, symbols, variables, allocated_variables) for arg in ir.args]
- )
+ args = reversed(_convert_ir_bb_list(ctx, ir.args, symbols))
topic_count = int(ir.value[3:])
assert topic_count >= 0 and topic_count <= 4, "invalid topic count"
ctx.get_basic_block().append_instruction("log", topic_count, *args)
elif isinstance(ir.value, str) and ir.value.upper() in get_opcodes():
- _convert_ir_opcode(ctx, ir, symbols, variables, allocated_variables)
+ _convert_ir_opcode(ctx, ir, symbols)
elif isinstance(ir.value, str) and ir.value in symbols:
return symbols[ir.value]
elif ir.is_literal:
@@ -862,28 +555,10 @@ def emit_body_blocks():
return None
-def _convert_ir_opcode(
- ctx: IRFunction,
- ir: IRnode,
- symbols: SymbolTable,
- variables: OrderedSet,
- allocated_variables: dict[str, IRVariable],
-) -> None:
+def _convert_ir_opcode(ctx: IRFunction, ir: IRnode, symbols: SymbolTable) -> None:
opcode = ir.value.upper() # type: ignore
inst_args = []
for arg in ir.args:
if isinstance(arg, IRnode):
- inst_args.append(_convert_ir_bb(ctx, arg, symbols, variables, allocated_variables))
+ inst_args.append(_convert_ir_bb(ctx, arg, symbols))
ctx.get_basic_block().append_instruction(opcode, *inst_args)
-
-
-def _data_ofst_of(sym, ofst, height_):
- # e.g. _OFST _sym_foo 32
- assert is_symbol(sym) or is_mem_sym(sym)
- if isinstance(ofst.value, int):
- # resolve at compile time using magic _OFST op
- return ["_OFST", sym, ofst.value]
- else:
- # if we can't resolve at compile time, resolve at runtime
- # ofst = _compile_to_assembly(ofst, withargs, existing_labels, break_dest, height_)
- return ofst + [sym, "ADD"]
diff --git a/vyper/venom/passes/base_pass.py b/vyper/venom/passes/base_pass.py
index 11da80ac66..3fbbdef6df 100644
--- a/vyper/venom/passes/base_pass.py
+++ b/vyper/venom/passes/base_pass.py
@@ -9,11 +9,13 @@ def run_pass(cls, *args, **kwargs):
t = cls()
count = 0
- while True:
+ for _ in range(1000):
changes_count = t._run_pass(*args, **kwargs) or 0
count += changes_count
if changes_count == 0:
break
+ else:
+ raise Exception("Too many iterations in IR pass!", t.__class__)
return count
diff --git a/vyper/venom/passes/dft.py b/vyper/venom/passes/dft.py
index 26994bd27f..5d149cf003 100644
--- a/vyper/venom/passes/dft.py
+++ b/vyper/venom/passes/dft.py
@@ -1,13 +1,31 @@
from vyper.utils import OrderedSet
from vyper.venom.analysis import DFG
-from vyper.venom.basicblock import IRBasicBlock, IRInstruction
+from vyper.venom.basicblock import BB_TERMINATORS, IRBasicBlock, IRInstruction, IRVariable
from vyper.venom.function import IRFunction
from vyper.venom.passes.base_pass import IRPass
-# DataFlow Transformation
class DFTPass(IRPass):
- def _process_instruction_r(self, bb: IRBasicBlock, inst: IRInstruction):
+ inst_order: dict[IRInstruction, int]
+ inst_order_num: int
+
+ def _process_instruction_r(self, bb: IRBasicBlock, inst: IRInstruction, offset: int = 0):
+ for op in inst.get_outputs():
+ assert isinstance(op, IRVariable), f"expected variable, got {op}"
+ uses = self.dfg.get_uses(op)
+
+ for uses_this in uses:
+ if uses_this.parent != inst.parent or uses_this.fence_id != inst.fence_id:
+ # don't reorder across basic block or fence boundaries
+ continue
+
+ # if the instruction is a terminator, we need to place
+ # it at the end of the basic block
+ # along with all the instructions that "lead" to it
+ if uses_this.opcode in BB_TERMINATORS:
+ offset = len(bb.instructions)
+ self._process_instruction_r(bb, uses_this, offset)
+
if inst in self.visited_instructions:
return
self.visited_instructions.add(inst)
@@ -15,35 +33,43 @@ def _process_instruction_r(self, bb: IRBasicBlock, inst: IRInstruction):
if inst.opcode == "phi":
# phi instructions stay at the beginning of the basic block
# and no input processing is needed
- bb.instructions.append(inst)
+ # bb.instructions.append(inst)
+ self.inst_order[inst] = 0
return
for op in inst.get_inputs():
target = self.dfg.get_producing_instruction(op)
+ assert target is not None, f"no producing instruction for {op}"
if target.parent != inst.parent or target.fence_id != inst.fence_id:
# don't reorder across basic block or fence boundaries
continue
- self._process_instruction_r(bb, target)
+ self._process_instruction_r(bb, target, offset)
- bb.instructions.append(inst)
+ self.inst_order_num += 1
+ self.inst_order[inst] = self.inst_order_num + offset
def _process_basic_block(self, bb: IRBasicBlock) -> None:
self.ctx.append_basic_block(bb)
- instructions = bb.instructions
- bb.instructions = []
-
- for inst in instructions:
+ for inst in bb.instructions:
inst.fence_id = self.fence_id
if inst.volatile:
self.fence_id += 1
- for inst in instructions:
+ # We go throught the instructions and calculate the order in which they should be executed
+ # based on the data flow graph. This order is stored in the inst_order dictionary.
+ # We then sort the instructions based on this order.
+ self.inst_order = {}
+ self.inst_order_num = 0
+ for inst in bb.instructions:
self._process_instruction_r(bb, inst)
+ bb.instructions.sort(key=lambda x: self.inst_order[x])
+
def _run_pass(self, ctx: IRFunction) -> None:
self.ctx = ctx
self.dfg = DFG.build_dfg(ctx)
+
self.fence_id = 0
self.visited_instructions: OrderedSet[IRInstruction] = OrderedSet()
diff --git a/vyper/venom/passes/make_ssa.py b/vyper/venom/passes/make_ssa.py
new file mode 100644
index 0000000000..06c61c9ea7
--- /dev/null
+++ b/vyper/venom/passes/make_ssa.py
@@ -0,0 +1,174 @@
+from vyper.utils import OrderedSet
+from vyper.venom.analysis import calculate_cfg, calculate_liveness
+from vyper.venom.basicblock import IRBasicBlock, IRInstruction, IROperand, IRVariable
+from vyper.venom.dominators import DominatorTree
+from vyper.venom.function import IRFunction
+from vyper.venom.passes.base_pass import IRPass
+
+
+class MakeSSA(IRPass):
+ """
+ This pass converts the function into Static Single Assignment (SSA) form.
+ """
+
+ dom: DominatorTree
+ defs: dict[IRVariable, OrderedSet[IRBasicBlock]]
+
+ def _run_pass(self, ctx: IRFunction, entry: IRBasicBlock) -> int:
+ self.ctx = ctx
+
+ calculate_cfg(ctx)
+ self.dom = DominatorTree.build_dominator_tree(ctx, entry)
+
+ calculate_liveness(ctx)
+ self._add_phi_nodes()
+
+ self.var_name_counters = {var.name: 0 for var in self.defs.keys()}
+ self.var_name_stacks = {var.name: [0] for var in self.defs.keys()}
+ self._rename_vars(entry)
+ self._remove_degenerate_phis(entry)
+
+ return 0
+
+ def _add_phi_nodes(self):
+ """
+ Add phi nodes to the function.
+ """
+ self._compute_defs()
+ work = {var: 0 for var in self.dom.dfs_walk}
+ has_already = {var: 0 for var in self.dom.dfs_walk}
+ i = 0
+
+ # Iterate over all variables
+ for var, d in self.defs.items():
+ i += 1
+ defs = list(d)
+ while len(defs) > 0:
+ bb = defs.pop()
+ for dom in self.dom.dominator_frontiers[bb]:
+ if has_already[dom] >= i:
+ continue
+
+ self._place_phi(var, dom)
+ has_already[dom] = i
+ if work[dom] < i:
+ work[dom] = i
+ defs.append(dom)
+
+ def _place_phi(self, var: IRVariable, basic_block: IRBasicBlock):
+ if var not in basic_block.in_vars:
+ return
+
+ args: list[IROperand] = []
+ for bb in basic_block.cfg_in:
+ if bb == basic_block:
+ continue
+
+ args.append(bb.label) # type: ignore
+ args.append(var) # type: ignore
+
+ basic_block.insert_instruction(IRInstruction("phi", args, var), 0)
+
+ def _add_phi(self, var: IRVariable, basic_block: IRBasicBlock) -> bool:
+ for inst in basic_block.instructions:
+ if inst.opcode == "phi" and inst.output is not None and inst.output.name == var.name:
+ return False
+
+ args: list[IROperand] = []
+ for bb in basic_block.cfg_in:
+ if bb == basic_block:
+ continue
+
+ args.append(bb.label)
+ args.append(var)
+
+ phi = IRInstruction("phi", args, var)
+ basic_block.instructions.insert(0, phi)
+
+ return True
+
+ def _rename_vars(self, basic_block: IRBasicBlock):
+ """
+ Rename variables. This follows the placement of phi nodes.
+ """
+ outs = []
+
+ # Pre-action
+ for inst in basic_block.instructions:
+ new_ops = []
+ if inst.opcode != "phi":
+ for op in inst.operands:
+ if not isinstance(op, IRVariable):
+ new_ops.append(op)
+ continue
+
+ new_ops.append(IRVariable(op.name, version=self.var_name_stacks[op.name][-1]))
+
+ inst.operands = new_ops
+
+ if inst.output is not None:
+ v_name = inst.output.name
+ i = self.var_name_counters[v_name]
+
+ self.var_name_stacks[v_name].append(i)
+ self.var_name_counters[v_name] = i + 1
+
+ inst.output = IRVariable(v_name, version=i)
+ # note - after previous line, inst.output.name != v_name
+ outs.append(inst.output.name)
+
+ for bb in basic_block.cfg_out:
+ for inst in bb.instructions:
+ if inst.opcode != "phi":
+ continue
+ assert inst.output is not None, "Phi instruction without output"
+ for i, op in enumerate(inst.operands):
+ if op == basic_block.label:
+ inst.operands[i + 1] = IRVariable(
+ inst.output.name, version=self.var_name_stacks[inst.output.name][-1]
+ )
+
+ for bb in self.dom.dominated[basic_block]:
+ if bb == basic_block:
+ continue
+ self._rename_vars(bb)
+
+ # Post-action
+ for op_name in outs:
+ # NOTE: each pop corresponds to an append in the pre-action above
+ self.var_name_stacks[op_name].pop()
+
+ def _remove_degenerate_phis(self, entry: IRBasicBlock):
+ for inst in entry.instructions.copy():
+ if inst.opcode != "phi":
+ continue
+
+ new_ops = []
+ for label, op in inst.phi_operands:
+ if op == inst.output:
+ continue
+ new_ops.extend([label, op])
+ new_ops_len = len(new_ops)
+ if new_ops_len == 0:
+ entry.instructions.remove(inst)
+ elif new_ops_len == 2:
+ entry.instructions.remove(inst)
+ else:
+ inst.operands = new_ops
+
+ for bb in self.dom.dominated[entry]:
+ if bb == entry:
+ continue
+ self._remove_degenerate_phis(bb)
+
+ def _compute_defs(self):
+ """
+ Compute the definition points of variables in the function.
+ """
+ self.defs = {}
+ for bb in self.dom.dfs_walk:
+ assignments = bb.get_assignments()
+ for var in assignments:
+ if var not in self.defs:
+ self.defs[var] = OrderedSet()
+ self.defs[var].add(bb)
diff --git a/vyper/venom/passes/normalization.py b/vyper/venom/passes/normalization.py
index 26699099b2..9ca8127b2d 100644
--- a/vyper/venom/passes/normalization.py
+++ b/vyper/venom/passes/normalization.py
@@ -28,7 +28,7 @@ def _insert_split_basicblock(self, bb: IRBasicBlock, in_bb: IRBasicBlock) -> IRB
source = in_bb.label.value
target = bb.label.value
- split_label = IRLabel(f"{target}_split_{source}")
+ split_label = IRLabel(f"{source}_split_{target}")
in_terminal = in_bb.instructions[-1]
in_terminal.replace_label_operands({bb.label: split_label})
@@ -36,6 +36,13 @@ def _insert_split_basicblock(self, bb: IRBasicBlock, in_bb: IRBasicBlock) -> IRB
split_bb.append_instruction("jmp", bb.label)
self.ctx.append_basic_block(split_bb)
+ for inst in bb.instructions:
+ if inst.opcode != "phi":
+ continue
+ for i in range(0, len(inst.operands), 2):
+ if inst.operands[i] == in_bb.label:
+ inst.operands[i] = split_bb.label
+
# Update the labels in the data segment
for inst in self.ctx.data_segment:
if inst.opcode == "db" and inst.operands[0] == bb.label:
@@ -55,5 +62,6 @@ def _run_pass(self, ctx: IRFunction) -> int:
# If we made changes, recalculate the cfg
if self.changes > 0:
calculate_cfg(ctx)
+ ctx.remove_unreachable_blocks()
return self.changes
diff --git a/vyper/venom/passes/simplify_cfg.py b/vyper/venom/passes/simplify_cfg.py
new file mode 100644
index 0000000000..7f02ccf819
--- /dev/null
+++ b/vyper/venom/passes/simplify_cfg.py
@@ -0,0 +1,82 @@
+from vyper.utils import OrderedSet
+from vyper.venom.basicblock import IRBasicBlock
+from vyper.venom.function import IRFunction
+from vyper.venom.passes.base_pass import IRPass
+
+
+class SimplifyCFGPass(IRPass):
+ visited: OrderedSet
+
+ def _merge_blocks(self, a: IRBasicBlock, b: IRBasicBlock):
+ a.instructions.pop()
+ for inst in b.instructions:
+ assert inst.opcode != "phi", "Not implemented yet"
+ if inst.opcode == "phi":
+ a.instructions.insert(0, inst)
+ else:
+ inst.parent = a
+ a.instructions.append(inst)
+
+ # Update CFG
+ a.cfg_out = b.cfg_out
+ if len(b.cfg_out) > 0:
+ next_bb = b.cfg_out.first()
+ next_bb.remove_cfg_in(b)
+ next_bb.add_cfg_in(a)
+
+ self.ctx.basic_blocks.remove(b)
+
+ def _merge_jump(self, a: IRBasicBlock, b: IRBasicBlock):
+ next_bb = b.cfg_out.first()
+ jump_inst = a.instructions[-1]
+ assert b.label in jump_inst.operands, f"{b.label} {jump_inst.operands}"
+ jump_inst.operands[jump_inst.operands.index(b.label)] = next_bb.label
+
+ # Update CFG
+ a.remove_cfg_out(b)
+ a.add_cfg_out(next_bb)
+ next_bb.remove_cfg_in(b)
+ next_bb.add_cfg_in(a)
+
+ self.ctx.basic_blocks.remove(b)
+
+ def _collapse_chained_blocks_r(self, bb: IRBasicBlock):
+ """
+ DFS into the cfg and collapse blocks with a single predecessor to the predecessor
+ """
+ if len(bb.cfg_out) == 1:
+ next_bb = bb.cfg_out.first()
+ if len(next_bb.cfg_in) == 1:
+ self._merge_blocks(bb, next_bb)
+ self._collapse_chained_blocks_r(bb)
+ return
+ elif len(bb.cfg_out) == 2:
+ bb_out = bb.cfg_out.copy()
+ for next_bb in bb_out:
+ if (
+ len(next_bb.cfg_in) == 1
+ and len(next_bb.cfg_out) == 1
+ and len(next_bb.instructions) == 1
+ ):
+ self._merge_jump(bb, next_bb)
+ self._collapse_chained_blocks_r(bb)
+ return
+
+ if bb in self.visited:
+ return
+ self.visited.add(bb)
+
+ for bb_out in bb.cfg_out:
+ self._collapse_chained_blocks_r(bb_out)
+
+ def _collapse_chained_blocks(self, entry: IRBasicBlock):
+ """
+ Collapse blocks with a single predecessor to their predecessor
+ """
+ self.visited = OrderedSet()
+ self._collapse_chained_blocks_r(entry)
+
+ def _run_pass(self, ctx: IRFunction, entry: IRBasicBlock) -> None:
+ self.ctx = ctx
+
+ self._collapse_chained_blocks(entry)
diff --git a/vyper/venom/passes/stack_reorder.py b/vyper/venom/passes/stack_reorder.py
new file mode 100644
index 0000000000..b32ec4abde
--- /dev/null
+++ b/vyper/venom/passes/stack_reorder.py
@@ -0,0 +1,24 @@
+from vyper.utils import OrderedSet
+from vyper.venom.basicblock import IRBasicBlock
+from vyper.venom.function import IRFunction
+from vyper.venom.passes.base_pass import IRPass
+
+
+class StackReorderPass(IRPass):
+ visited: OrderedSet
+
+ def _reorder_stack(self, bb: IRBasicBlock):
+ pass
+
+ def _visit(self, bb: IRBasicBlock):
+ if bb in self.visited:
+ return
+ self.visited.add(bb)
+
+ for bb_out in bb.cfg_out:
+ self._visit(bb_out)
+
+ def _run_pass(self, ctx: IRFunction, entry: IRBasicBlock):
+ self.ctx = ctx
+ self.visited = OrderedSet()
+ self._visit(entry)
diff --git a/vyper/venom/stack_model.py b/vyper/venom/stack_model.py
index 66c62b74d2..a98e5bb25b 100644
--- a/vyper/venom/stack_model.py
+++ b/vyper/venom/stack_model.py
@@ -30,34 +30,36 @@ def push(self, op: IROperand) -> None:
def pop(self, num: int = 1) -> None:
del self._stack[len(self._stack) - num :]
- def get_depth(self, op: IROperand) -> int:
+ def get_depth(self, op: IROperand, n: int = 1) -> int:
"""
- Returns the depth of the first matching operand in the stack map.
+ Returns the depth of the n-th matching operand in the stack map.
If the operand is not in the stack map, returns NOT_IN_STACK.
"""
assert isinstance(op, IROperand), f"{type(op)}: {op}"
for i, stack_op in enumerate(reversed(self._stack)):
if stack_op.value == op.value:
- return -i
+ if n <= 1:
+ return -i
+ else:
+ n -= 1
return StackModel.NOT_IN_STACK # type: ignore
- def get_phi_depth(self, phi1: IRVariable, phi2: IRVariable) -> int:
+ def get_phi_depth(self, phis: list[IRVariable]) -> int:
"""
Returns the depth of the first matching phi variable in the stack map.
If the none of the phi operands are in the stack, returns NOT_IN_STACK.
- Asserts that exactly one of phi1 and phi2 is found.
+ Asserts that exactly one of phis is found.
"""
- assert isinstance(phi1, IRVariable)
- assert isinstance(phi2, IRVariable)
+ assert isinstance(phis, list)
ret = StackModel.NOT_IN_STACK
for i, stack_item in enumerate(reversed(self._stack)):
- if stack_item in (phi1, phi2):
+ if stack_item in phis:
assert (
ret is StackModel.NOT_IN_STACK
- ), f"phi argument is not unique! {phi1}, {phi2}, {self._stack}"
+ ), f"phi argument is not unique! {phis}, {self._stack}"
ret = -i
return ret # type: ignore
diff --git a/vyper/venom/venom_to_assembly.py b/vyper/venom/venom_to_assembly.py
index 608e100cd1..0cb13becf2 100644
--- a/vyper/venom/venom_to_assembly.py
+++ b/vyper/venom/venom_to_assembly.py
@@ -1,8 +1,22 @@
+from collections import Counter
from typing import Any
-from vyper.ir.compile_ir import PUSH, DataHeader, RuntimeHeader, optimize_assembly
+from vyper.exceptions import CompilerPanic, StackTooDeep
+from vyper.ir.compile_ir import (
+ PUSH,
+ DataHeader,
+ Instruction,
+ RuntimeHeader,
+ mksymbol,
+ optimize_assembly,
+)
from vyper.utils import MemoryPositions, OrderedSet
-from vyper.venom.analysis import calculate_cfg, calculate_liveness, input_vars_from
+from vyper.venom.analysis import (
+ calculate_cfg,
+ calculate_dup_requirements,
+ calculate_liveness,
+ input_vars_from,
+)
from vyper.venom.basicblock import (
IRBasicBlock,
IRInstruction,
@@ -10,7 +24,6 @@
IRLiteral,
IROperand,
IRVariable,
- MemType,
)
from vyper.venom.function import IRFunction
from vyper.venom.passes.normalization import NormalizationPass
@@ -23,15 +36,18 @@
"coinbase",
"calldatasize",
"calldatacopy",
+ "mcopy",
"calldataload",
"gas",
"gasprice",
"gaslimit",
+ "chainid",
"address",
"origin",
"number",
"extcodesize",
"extcodehash",
+ "extcodecopy",
"returndatasize",
"returndatacopy",
"callvalue",
@@ -40,13 +56,17 @@
"sstore",
"mload",
"mstore",
+ "tload",
+ "tstore",
"timestamp",
"caller",
+ "blockhash",
"selfdestruct",
"signextend",
"stop",
"shr",
"shl",
+ "sar",
"and",
"xor",
"or",
@@ -54,8 +74,13 @@
"sub",
"mul",
"div",
+ "smul",
+ "sdiv",
"mod",
+ "smod",
"exp",
+ "addmod",
+ "mulmod",
"eq",
"iszero",
"not",
@@ -63,12 +88,34 @@
"lt",
"slt",
"sgt",
+ "create",
+ "create2",
+ "msize",
+ "balance",
+ "call",
+ "staticcall",
+ "delegatecall",
+ "codesize",
+ "basefee",
+ "prevrandao",
+ "difficulty",
+ "invalid",
]
)
_REVERT_POSTAMBLE = ["_sym___revert", "JUMPDEST", *PUSH(0), "DUP1", "REVERT"]
+def apply_line_numbers(inst: IRInstruction, asm) -> list[str]:
+ ret = []
+ for op in asm:
+ if isinstance(op, str) and not isinstance(op, Instruction):
+ ret.append(Instruction(op, inst.ast_source, inst.error_msg))
+ else:
+ ret.append(op)
+ return ret # type: ignore
+
+
# TODO: "assembly" gets into the recursion due to how the original
# IR was structured recursively in regards with the deploy instruction.
# There, recursing into the deploy instruction was by design, and
@@ -105,18 +152,18 @@ def generate_evm(self, no_optimize: bool = False) -> list[str]:
# This is a side-effect of how dynamic jumps are temporarily being used
# to support the O(1) dispatcher. -> look into calculate_cfg()
for ctx in self.ctxs:
- calculate_cfg(ctx)
NormalizationPass.run_pass(ctx)
+ calculate_cfg(ctx)
calculate_liveness(ctx)
+ calculate_dup_requirements(ctx)
assert ctx.normalized, "Non-normalized CFG!"
self._generate_evm_for_basicblock_r(asm, ctx.basic_blocks[0], StackModel())
# TODO make this property on IRFunction
+ asm.extend(["_sym__ctor_exit", "JUMPDEST"])
if ctx.immutables_len is not None and ctx.ctor_mem_size is not None:
- while asm[-1] != "JUMPDEST":
- asm.pop()
asm.extend(
["_sym_subcode_size", "_sym_runtime_begin", "_mem_deploy_start", "CODECOPY"]
)
@@ -139,7 +186,11 @@ def generate_evm(self, no_optimize: bool = False) -> list[str]:
label = inst.operands[0].value
data_segments[label] = [DataHeader(f"_sym_{label}")]
elif inst.opcode == "db":
- data_segments[label].append(f"_sym_{inst.operands[0].value}")
+ data = inst.operands[0]
+ if isinstance(data, IRLabel):
+ data_segments[label].append(f"_sym_{data.value}")
+ else:
+ data_segments[label].append(data)
asm.extend(list(data_segments.values()))
@@ -149,20 +200,27 @@ def generate_evm(self, no_optimize: bool = False) -> list[str]:
return top_asm
def _stack_reorder(
- self, assembly: list, stack: StackModel, _stack_ops: OrderedSet[IRVariable]
+ self, assembly: list, stack: StackModel, stack_ops: list[IRVariable]
) -> None:
- # make a list so we can index it
- stack_ops = [x for x in _stack_ops.keys()]
- stack_ops_count = len(_stack_ops)
+ stack_ops_count = len(stack_ops)
+
+ counts = Counter(stack_ops)
for i in range(stack_ops_count):
op = stack_ops[i]
final_stack_depth = -(stack_ops_count - i - 1)
- depth = stack.get_depth(op) # type: ignore
+ depth = stack.get_depth(op, counts[op]) # type: ignore
+ counts[op] -= 1
+
+ if depth == StackModel.NOT_IN_STACK:
+ raise CompilerPanic(f"Variable {op} not in stack")
if depth == final_stack_depth:
continue
+ if op == stack.peek(final_stack_depth):
+ continue
+
self.swap(assembly, stack, depth)
self.swap(assembly, stack, final_stack_depth)
@@ -192,23 +250,20 @@ def _emit_input_operands(
continue
if isinstance(op, IRLiteral):
- assembly.extend([*PUSH(op.value)])
+ if op.value < -(2**255):
+ raise Exception(f"Value too low: {op.value}")
+ elif op.value >= 2**256:
+ raise Exception(f"Value too high: {op.value}")
+ assembly.extend(PUSH(op.value % 2**256))
stack.push(op)
continue
- if op in inst.dup_requirements:
+ if op in inst.dup_requirements and op not in emitted_ops:
self.dup_op(assembly, stack, op)
if op in emitted_ops:
self.dup_op(assembly, stack, op)
- # REVIEW: this seems like it can be reordered across volatile
- # boundaries (which includes memory fences). maybe just
- # remove it entirely at this point
- if isinstance(op, IRVariable) and op.mem_type == MemType.MEMORY:
- assembly.extend([*PUSH(op.mem_addr)])
- assembly.append("MLOAD")
-
emitted_ops.add(op)
def _generate_evm_for_basicblock_r(
@@ -224,12 +279,34 @@ def _generate_evm_for_basicblock_r(
self.clean_stack_from_cfg_in(asm, basicblock, stack)
- for inst in basicblock.instructions:
- asm = self._generate_evm_for_instruction(asm, inst, stack)
+ param_insts = [inst for inst in basicblock.instructions if inst.opcode == "param"]
+ main_insts = [inst for inst in basicblock.instructions if inst.opcode != "param"]
+
+ for inst in param_insts:
+ asm.extend(self._generate_evm_for_instruction(inst, stack))
+
+ self._clean_unused_params(asm, basicblock, stack)
+
+ for i, inst in enumerate(main_insts):
+ next_liveness = main_insts[i + 1].liveness if i + 1 < len(main_insts) else OrderedSet()
- for bb in basicblock.cfg_out:
+ asm.extend(self._generate_evm_for_instruction(inst, stack, next_liveness))
+
+ for bb in basicblock.reachable:
self._generate_evm_for_basicblock_r(asm, bb, stack.copy())
+ def _clean_unused_params(self, asm: list, bb: IRBasicBlock, stack: StackModel) -> None:
+ for i, inst in enumerate(bb.instructions):
+ if inst.opcode != "param":
+ break
+ if inst.volatile and i + 1 < len(bb.instructions):
+ liveness = bb.instructions[i + 1].liveness
+ if inst.output is not None and inst.output not in liveness:
+ depth = stack.get_depth(inst.output)
+ if depth != 0:
+ self.swap(asm, stack, depth)
+ self.pop(asm, stack)
+
# pop values from stack at entry to bb
# note this produces the same result(!) no matter which basic block
# we enter from in the CFG.
@@ -258,12 +335,14 @@ def clean_stack_from_cfg_in(
continue
if depth != 0:
- stack.swap(depth)
+ self.swap(asm, stack, depth)
self.pop(asm, stack)
def _generate_evm_for_instruction(
- self, assembly: list, inst: IRInstruction, stack: StackModel
+ self, inst: IRInstruction, stack: StackModel, next_liveness: OrderedSet = None
) -> list[str]:
+ assembly: list[str | int] = []
+ next_liveness = next_liveness or OrderedSet()
opcode = inst.opcode
#
@@ -276,10 +355,22 @@ def _generate_evm_for_instruction(
operands = inst.get_non_label_operands()
elif opcode == "alloca":
operands = inst.operands[1:2]
+
+ # iload and istore are special cases because they can take a literal
+ # that is handled specialy with the _OFST macro. Look below, after the
+ # stack reordering.
elif opcode == "iload":
- operands = []
+ addr = inst.operands[0]
+ if isinstance(addr, IRLiteral):
+ operands = []
+ else:
+ operands = inst.operands
elif opcode == "istore":
- operands = inst.operands[0:1]
+ addr = inst.operands[1]
+ if isinstance(addr, IRLiteral):
+ operands = inst.operands[:1]
+ else:
+ operands = inst.operands
elif opcode == "log":
log_topic_count = inst.operands[0].value
assert log_topic_count in [0, 1, 2, 3, 4], "Invalid topic count"
@@ -289,8 +380,8 @@ def _generate_evm_for_instruction(
if opcode == "phi":
ret = inst.get_outputs()[0]
- phi1, phi2 = inst.get_inputs()
- depth = stack.get_phi_depth(phi1, phi2)
+ phis = inst.get_inputs()
+ depth = stack.get_phi_depth(phis)
# collapse the arguments to the phi node in the stack.
# example, for `%56 = %label1 %13 %label2 %14`, we will
# find an instance of %13 *or* %14 in the stack and replace it with %56.
@@ -301,7 +392,7 @@ def _generate_evm_for_instruction(
stack.poke(0, ret)
else:
stack.poke(depth, ret)
- return assembly
+ return apply_line_numbers(inst, assembly)
# Step 2: Emit instruction's input operands
self._emit_input_operands(assembly, inst, operands, stack)
@@ -313,11 +404,15 @@ def _generate_evm_for_instruction(
b = next(iter(inst.parent.cfg_out))
target_stack = input_vars_from(inst.parent, b)
# TODO optimize stack reordering at entry and exit from basic blocks
- self._stack_reorder(assembly, stack, target_stack)
+ # NOTE: stack in general can contain multiple copies of the same variable,
+ # however we are safe in the case of jmp/djmp/jnz as it's not going to
+ # have multiples.
+ target_stack_list = list(target_stack)
+ self._stack_reorder(assembly, stack, target_stack_list)
# final step to get the inputs to this instruction ordered
# correctly on the stack
- self._stack_reorder(assembly, stack, OrderedSet(operands))
+ self._stack_reorder(assembly, stack, operands) # type: ignore
# some instructions (i.e. invoke) need to do stack manipulations
# with the stack model containing the return value(s), so we fiddle
@@ -359,7 +454,9 @@ def _generate_evm_for_instruction(
assembly.append(f"_sym_{inst.operands[0].value}")
assembly.append("JUMP")
elif opcode == "djmp":
- assert isinstance(inst.operands[0], IRVariable)
+ assert isinstance(
+ inst.operands[0], IRVariable
+ ), f"Expected IRVariable, got {inst.operands[0]}"
assembly.append("JUMP")
elif opcode == "gt":
assembly.append("GT")
@@ -367,7 +464,9 @@ def _generate_evm_for_instruction(
assembly.append("LT")
elif opcode == "invoke":
target = inst.operands[0]
- assert isinstance(target, IRLabel), "invoke target must be a label"
+ assert isinstance(
+ target, IRLabel
+ ), f"invoke target must be a label (is ${type(target)} ${target})"
assembly.extend(
[
f"_sym_label_ret_{self.label_counter}",
@@ -378,16 +477,12 @@ def _generate_evm_for_instruction(
]
)
self.label_counter += 1
- if stack.height > 0 and stack.peek(0) in inst.dup_requirements:
- self.pop(assembly, stack)
- elif opcode == "call":
- assembly.append("CALL")
- elif opcode == "staticcall":
- assembly.append("STATICCALL")
elif opcode == "ret":
assembly.append("JUMP")
elif opcode == "return":
assembly.append("RETURN")
+ elif opcode == "exit":
+ assembly.extend(["_sym__ctor_exit", "JUMP"])
elif opcode == "phi":
pass
elif opcode == "sha3":
@@ -395,10 +490,10 @@ def _generate_evm_for_instruction(
elif opcode == "sha3_64":
assembly.extend(
[
- *PUSH(MemoryPositions.FREE_VAR_SPACE2),
- "MSTORE",
*PUSH(MemoryPositions.FREE_VAR_SPACE),
"MSTORE",
+ *PUSH(MemoryPositions.FREE_VAR_SPACE2),
+ "MSTORE",
*PUSH(64),
*PUSH(MemoryPositions.FREE_VAR_SPACE),
"SHA3",
@@ -408,12 +503,23 @@ def _generate_evm_for_instruction(
assembly.extend([*PUSH(31), "ADD", *PUSH(31), "NOT", "AND"])
elif opcode == "assert":
assembly.extend(["ISZERO", "_sym___revert", "JUMPI"])
+ elif opcode == "assert_unreachable":
+ end_symbol = mksymbol("reachable")
+ assembly.extend([end_symbol, "JUMPI", "INVALID", end_symbol, "JUMPDEST"])
elif opcode == "iload":
- loc = inst.operands[0].value
- assembly.extend(["_OFST", "_mem_deploy_end", loc, "MLOAD"])
+ addr = inst.operands[0]
+ if isinstance(addr, IRLiteral):
+ assembly.extend(["_OFST", "_mem_deploy_end", addr.value])
+ else:
+ assembly.extend(["_mem_deploy_end", "ADD"])
+ assembly.append("MLOAD")
elif opcode == "istore":
- loc = inst.operands[1].value
- assembly.extend(["_OFST", "_mem_deploy_end", loc, "MSTORE"])
+ addr = inst.operands[1]
+ if isinstance(addr, IRLiteral):
+ assembly.extend(["_OFST", "_mem_deploy_end", addr.value])
+ else:
+ assembly.extend(["_mem_deploy_end", "ADD"])
+ assembly.append("MSTORE")
elif opcode == "log":
assembly.extend([f"LOG{log_topic_count}"])
else:
@@ -421,19 +527,20 @@ def _generate_evm_for_instruction(
# Step 6: Emit instructions output operands (if any)
if inst.output is not None:
- assert isinstance(inst.output, IRVariable), "Return value must be a variable"
- if inst.output.mem_type == MemType.MEMORY:
- assembly.extend([*PUSH(inst.output.mem_addr)])
+ if "call" in inst.opcode and inst.output not in next_liveness:
+ self.pop(assembly, stack)
- return assembly
+ return apply_line_numbers(inst, assembly)
def pop(self, assembly, stack, num=1):
stack.pop(num)
assembly.extend(["POP"] * num)
def swap(self, assembly, stack, depth):
+ # Swaps of the top is no op
if depth == 0:
return
+
stack.swap(depth)
assembly.append(_evm_swap_for(depth))
@@ -450,11 +557,13 @@ def dup_op(self, assembly, stack, op):
def _evm_swap_for(depth: int) -> str:
swap_idx = -depth
- assert 1 <= swap_idx <= 16, "Unsupported swap depth"
+ if not (1 <= swap_idx <= 16):
+ raise StackTooDeep(f"Unsupported swap depth {swap_idx}")
return f"SWAP{swap_idx}"
def _evm_dup_for(depth: int) -> str:
dup_idx = 1 - depth
- assert 1 <= dup_idx <= 16, "Unsupported dup depth"
+ if not (1 <= dup_idx <= 16):
+ raise StackTooDeep(f"Unsupported dup depth {dup_idx}")
return f"DUP{dup_idx}"