Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat[venom]: implement new calling convention #4482

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
def test_call_unused_param_return_tuple(get_contract):
code = """
@internal
def _foo(a: uint256, b: uint256) -> (uint256, uint256):
return a, b

@external
def foo() -> (uint256, uint256):
return self._foo(1, 2)
"""

c = get_contract(code)

assert c.foo() == (1, 2)


def test_returning_immutables(get_contract):
"""
This test checks that we can return an immutable from an internal function, which results in
the immutable being copied into the return buffer with `dloadbytes`.
"""
contract = """
a: immutable(uint256)

@deploy
def __init__():
a = 5

@internal
def get_my_immutable() -> uint256:
return a

@external
def get_immutable() -> uint256:
return self.get_my_immutable()
"""
c = get_contract(contract)
assert c.get_immutable() == 5
4 changes: 2 additions & 2 deletions tests/functional/venom/parser/test_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ def test_multi_function():
expected_ctx.add_function(entry_fn := IRFunction(IRLabel("entry")))

entry_bb = entry_fn.get_basic_block("entry")
entry_bb.append_instruction("invoke", IRLabel("check_cv"))
entry_bb.append_invoke_instruction([IRLabel("check_cv")], returns=False)
entry_bb.append_instruction("jmp", IRLabel("wow"))

entry_fn.append_basic_block(wow_bb := IRBasicBlock(IRLabel("wow"), entry_fn))
Expand Down Expand Up @@ -213,7 +213,7 @@ def test_multi_function_and_data():
expected_ctx.add_function(entry_fn := IRFunction(IRLabel("entry")))

entry_bb = entry_fn.get_basic_block("entry")
entry_bb.append_instruction("invoke", IRLabel("check_cv"))
entry_bb.append_invoke_instruction([IRLabel("check_cv")], returns=False)
entry_bb.append_instruction("jmp", IRLabel("wow"))

entry_fn.append_basic_block(wow_bb := IRBasicBlock(IRLabel("wow"), entry_fn))
Expand Down
5 changes: 3 additions & 2 deletions tests/unit/compiler/test_source_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,9 @@ def foo(i: uint256):
raise self.bar(5%i)

@pure
def bar(i: uint256) -> String[32]:
return "foo foo"
def bar(i: uint256) -> String[85]:
# ensure the mod doesn't get erased
return concat("foo ", uint2str(i))
"""
error_map = compile_code(code, output_formats=["source_map"])["source_map"]["error_map"]
assert "user revert with reason" in error_map.values()
Expand Down
2 changes: 2 additions & 0 deletions vyper/codegen/arithmetic.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,8 @@ def safe_div(x, y):
def safe_mod(x, y):
typ = x.typ
MOD = "smod" if typ.is_signed else "mod"
# TODO: (force) propagate safemod error msg down to all children,
# overriding the "clamp" error msg.
return IRnode.from_list([MOD, x, clamp("gt", y, 0)], error_msg="safemod")


Expand Down
1 change: 0 additions & 1 deletion vyper/venom/basicblock.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@
"selfdestruct",
"stop",
"invalid",
"invoke",
"jmp",
"djmp",
"jnz",
Expand Down
161 changes: 150 additions & 11 deletions vyper/venom/ir_node_to_venom.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import functools
import re
from collections import defaultdict
from typing import Optional

from vyper.codegen.context import Alloca
from vyper.codegen.ir_node import IRnode
from vyper.evm.opcodes import get_opcodes
from vyper.venom.basicblock import (
Expand All @@ -13,7 +15,9 @@
IRVariable,
)
from vyper.venom.context import IRContext
from vyper.venom.function import IRFunction
from vyper.venom.function import IRFunction, IRParameter

ENABLE_NEW_CALL_CONV = True

# Instructions that are mapped to their inverse
INVERSE_MAPPED_IR_INSTRUCTIONS = {"ne": "eq", "le": "gt", "sle": "sgt", "ge": "lt", "sge": "slt"}
Expand Down Expand Up @@ -63,7 +67,6 @@
"gasprice",
"gaslimit",
"returndatasize",
"mload",
"iload",
"istore",
"dload",
Expand Down Expand Up @@ -109,15 +112,17 @@

SymbolTable = dict[str, IROperand]
_alloca_table: dict[int, IROperand]
_callsites: dict[str, list[Alloca]]
MAIN_ENTRY_LABEL_NAME = "__main_entry"


# convert IRnode directly to venom
def ir_node_to_venom(ir: IRnode) -> IRContext:
_ = ir.unique_symbols # run unique symbols check

global _alloca_table
global _alloca_table, _callsites
_alloca_table = {}
_callsites = defaultdict(list)

ctx = IRContext()
fn = ctx.create_function(MAIN_ENTRY_LABEL_NAME)
Expand Down Expand Up @@ -158,66 +163,151 @@ def _append_return_args(fn: IRFunction, ofst: int = 0, size: int = 0):


def _handle_self_call(fn: IRFunction, ir: IRnode, symbols: SymbolTable) -> Optional[IROperand]:
global _callsites
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
ret_args: list[IROperand] = [IRLabel(target_label)] # type: ignore

func_t = ir.passthrough_metadata["func_t"]
assert func_t is not None, "func_t not found in passthrough metadata"

returns_word = _returns_word(func_t)


if setup_ir != goto_ir:
_convert_ir_bb(fn, setup_ir, symbols)

converted_args = _convert_ir_bb_list(fn, goto_ir.args[1:], symbols)

callsite_op = converted_args[-1]
assert isinstance(callsite_op, IRLabel), converted_args
callsite = callsite_op.value

bb = fn.get_basic_block()
return_buf = None

if len(converted_args) > 1:
return_buf = converted_args[0]

if return_buf is not None:
ret_args.append(return_buf) # type: ignore
stack_args: list[IROperand] = [IRLabel(target_label)]

bb.append_invoke_instruction(ret_args, returns=False) # type: ignore
if return_buf is not None:
if not ENABLE_NEW_CALL_CONV or not returns_word:
stack_args.append(return_buf) # type: ignore

callsite_args = _callsites[callsite]
if ENABLE_NEW_CALL_CONV:
for alloca in callsite_args:
if not _is_word_type(alloca.typ):
continue
ptr = _alloca_table[alloca._id]
stack_arg = bb.append_instruction("mload", ptr)
assert stack_arg is not None
stack_args.append(stack_arg)

if returns_word:
ret_value = bb.append_invoke_instruction(stack_args, returns=True) # type: ignore
assert ret_value is not None
assert isinstance(return_buf, IROperand)
bb.append_instruction("mstore", ret_value, return_buf)
return return_buf

bb.append_invoke_instruction(stack_args, returns=False) # type: ignore

return return_buf


_current_func_t = None
_current_context = None


def _is_word_type(typ):
return typ._is_prim_word
# return typ.memory_bytes_required == 32


# func_t: ContractFunctionT
def _returns_word(func_t) -> bool:
return_t = func_t.return_type
return return_t is not None and _is_word_type(return_t)


def _handle_internal_func(
# TODO: remove does_return_data, replace with `func_t.return_type is not None`
fn: IRFunction,
ir: IRnode,
does_return_data: bool,
symbols: SymbolTable,
) -> IRFunction:
global _alloca_table
global _alloca_table, _current_func_t, _current_context

_current_func_t = ir.passthrough_metadata["func_t"]
_current_context = ir.passthrough_metadata["context"]
func_t = _current_func_t
context = _current_context

fn = fn.ctx.create_function(ir.args[0].args[0].value)

if ENABLE_NEW_CALL_CONV:
index = 0
if func_t.return_type is not None and not _returns_word(func_t):
index += 1
for arg in func_t.arguments:
var = context.lookup_var(arg.name)
if not _is_word_type(var.typ):
continue
venom_arg = IRParameter(
var.name, index, var.alloca.offset, var.alloca.size, None, None, None
)
fn.args.append(venom_arg)
index += 1

bb = fn.get_basic_block()

_saved_alloca_table = _alloca_table
_alloca_table = {}

returns_word = _returns_word(func_t)

# return buffer
if does_return_data:
buf = bb.append_instruction("param")
bb.instructions[-1].annotation = "return_buffer"
if ENABLE_NEW_CALL_CONV and returns_word:
# TODO: remove this once we have proper memory allocator
# functionality in venom. Currently, we hardcode the scratch
# buffer size of 32 bytes.
# TODO: we don't need to use scratch space once the legacy optimizer
# is disabled.
buf = bb.append_instruction(
"alloca", IRLiteral(0), IRLiteral(32), IRLiteral(99999999999999999)
)
else:
buf = bb.append_instruction("param")
bb.instructions[-1].annotation = "return_buffer"

assert buf is not None # help mypy
symbols["return_buffer"] = buf

if ENABLE_NEW_CALL_CONV:
for arg in fn.args:
ret = bb.append_instruction("param")
bb.instructions[-1].annotation = arg.name
assert ret is not None # help mypy
symbols[arg.name] = ret
arg.func_var = ret

# return address
return_pc = bb.append_instruction("param")
assert return_pc is not None # help mypy
symbols["return_pc"] = return_pc

bb.instructions[-1].annotation = "return_pc"

if ENABLE_NEW_CALL_CONV:
for arg in fn.args:
var = IRVariable(arg.name)
bb.append_instruction("store", IRLiteral(arg.offset), ret=var) # type: ignore
arg.addr_var = var

_convert_ir_bb(fn, ir.args[0].args[2], symbols)

_alloca_table = _saved_alloca_table
Expand Down Expand Up @@ -431,7 +521,13 @@ def _convert_ir_bb(fn, ir, symbols):
if label.value == "return_pc":
label = symbols.get("return_pc")
# return label should be top of stack
bb.append_instruction("ret", label)
if _returns_word(_current_func_t) and ENABLE_NEW_CALL_CONV:
buf = symbols["return_buffer"]
val = bb.append_instruction("mload", buf)
bb.append_instruction("ret", val, label)
else:
bb.append_instruction("ret", label)

else:
bb.append_instruction("jmp", label)

Expand All @@ -440,7 +536,31 @@ def _convert_ir_bb(fn, ir, symbols):
# to fix upstream.
val, ptr = _convert_ir_bb_list(fn, reversed(ir.args), symbols)

if ENABLE_NEW_CALL_CONV:
if isinstance(ptr, IRVariable):
# TODO: is this bad code?
param = fn.get_param_by_name(ptr)
if param is not None:
return fn.get_basic_block().append_instruction("store", val, ret=param.func_var)

if isinstance(ptr, IRLabel) and ptr.value.startswith("$palloca"):
symbol = symbols.get(ptr.annotation, None)
if symbol is not None:
return fn.get_basic_block().append_instruction("store", symbol)

return fn.get_basic_block().append_instruction("mstore", val, ptr)
elif ir.value == "mload":
arg = ir.args[0]
ptr = _convert_ir_bb(fn, arg, symbols)

if ENABLE_NEW_CALL_CONV:
if isinstance(arg.value, str) and arg.value.startswith("$palloca"):
symbol = symbols.get(arg.annotation, None)
if symbol is not None:
return fn.get_basic_block().append_instruction("store", symbol)

return fn.get_basic_block().append_instruction("mload", ptr)

elif ir.value == "ceil32":
x = ir.args[0]
expanded = IRnode.from_list(["and", ["add", x, 31], ["not", 31]])
Expand Down Expand Up @@ -550,12 +670,31 @@ def emit_body_blocks():

elif ir.value.startswith("$palloca"):
alloca = ir.passthrough_metadata["alloca"]
if ENABLE_NEW_CALL_CONV and fn.get_param_at_offset(alloca.offset) is not None:
return fn.get_param_at_offset(alloca.offset).addr_var
if alloca._id not in _alloca_table:
ptr = fn.get_basic_block().append_instruction(
"palloca", alloca.offset, alloca.size, alloca._id
)
_alloca_table[alloca._id] = ptr
return _alloca_table[alloca._id]
elif ir.value.startswith("$calloca"):
global _callsites
alloca = ir.passthrough_metadata["alloca"]
assert alloca._callsite is not None
if alloca._id not in _alloca_table:
bb = fn.get_basic_block()
if ENABLE_NEW_CALL_CONV and _is_word_type(alloca.typ):
ptr = bb.append_instruction("alloca", alloca.offset, alloca.size, alloca._id)
else:
ptr = IRLiteral(alloca.offset)

_alloca_table[alloca._id] = ptr
ret = _alloca_table[alloca._id]
# assumption: callocas appear in the same order as the
# order of arguments to the function.
_callsites[alloca._callsite].append(alloca)
return ret

elif ir.value.startswith("$calloca"):
alloca = ir.passthrough_metadata["alloca"]
Expand Down
Loading
Loading