diff --git a/tests/unit/cli/vyper_json/test_compile_json.py b/tests/unit/cli/vyper_json/test_compile_json.py index 3c0b356e9b..2a07ba6f3c 100644 --- a/tests/unit/cli/vyper_json/test_compile_json.py +++ b/tests/unit/cli/vyper_json/test_compile_json.py @@ -208,11 +208,13 @@ def test_compile_json(input_json, input_bundle, experimental_codegen): "object": data["bytecode"], "opcodes": data["opcodes"], "sourceMap": data["source_map"], + "symbolMap": data["symbol_map"], }, "deployedBytecode": { "object": data["bytecode_runtime"], "opcodes": data["opcodes_runtime"], "sourceMap": data["source_map_runtime"], + "symbolMap": data["symbol_map_runtime"], }, "methodIdentifiers": data["method_identifiers"], }, diff --git a/tests/unit/compiler/test_symbol_map.py b/tests/unit/compiler/test_symbol_map.py new file mode 100644 index 0000000000..fa6a84da94 --- /dev/null +++ b/tests/unit/compiler/test_symbol_map.py @@ -0,0 +1,43 @@ +from vyper.compiler import compile_code +from vyper.compiler.settings import Settings + +TEST_CODE = """ +@internal +def foo(a: uint256) -> uint256: + return a + 1 + +# force foo to not be inlined +@external +def bar(a: uint256) -> uint256: + return self.foo(a) + +@external +def baz(a: uint256) -> uint256: + return self.foo(a + 1) +""" + + +def test_simple_map(): + code = TEST_CODE + output = compile_code( + code, + output_formats=["symbol_map_runtime", "metadata"], + settings=Settings(experimental_codegen=True), + ) + meta = output["metadata"] + symbol_map = output["symbol_map_runtime"] + foo_meta_ent = None + assert "function_info" in meta, "missing function info in metadata" + function_infos = meta["function_info"] + assert isinstance(function_infos, dict), "function info is not a dict" + for _, v in function_infos.items(): + if v["name"] == "foo" and v["visibility"] == "internal": + foo_meta_ent = v + break + assert foo_meta_ent is not None, "didn't find entry for foo" + assert "venom_via_stack" in foo_meta_ent, "no stack info" + assert foo_meta_ent.get("venom_return_via_stack", False), "unexpected non-stack return" + assert foo_meta_ent["venom_via_stack"] == ["a"] + foo_id = foo_meta_ent["_ir_identifier"] + symbol_map_key = foo_id + "_runtime" + assert symbol_map_key in symbol_map, "missing constant start for foo()" diff --git a/vyper/cli/vyper_compile.py b/vyper/cli/vyper_compile.py index 83c57cf87f..eadabf302f 100755 --- a/vyper/cli/vyper_compile.py +++ b/vyper/cli/vyper_compile.py @@ -31,6 +31,8 @@ userdoc - Natspec user documentation devdoc - Natspec developer documentation metadata - Contract metadata (intended for use by tooling developers) +symbol_map_runtime - Symbol values in runtime bytecode (intended for use by tooling developers) +symbol_map - Symbol values in deployable bytecode (intended for use by tooling developers) combined_json - All of the above format options combined as single JSON output layout - Storage layout of a Vyper contract ast - AST (not yet annotated) in JSON format diff --git a/vyper/cli/vyper_json.py b/vyper/cli/vyper_json.py index 750afcb88e..865a9bd173 100755 --- a/vyper/cli/vyper_json.py +++ b/vyper/cli/vyper_json.py @@ -24,9 +24,11 @@ "evm.bytecode.object": "bytecode", "evm.bytecode.opcodes": "opcodes", "evm.bytecode.sourceMap": "source_map", + "evm.bytecode.symbolMap": "symbol_map", "evm.deployedBytecode.object": "bytecode_runtime", "evm.deployedBytecode.opcodes": "opcodes_runtime", "evm.deployedBytecode.sourceMap": "source_map_runtime", + "evm.deployedBytecode.symbolMap": "symbol_map_runtime", "interface": "interface", "ir": "ir_dict", "ir_runtime": "ir_runtime_dict", @@ -425,6 +427,8 @@ def format_to_output_dict(compiler_data: dict) -> dict: evm["opcodes"] = data["opcodes"] if "source_map" in data: evm["sourceMap"] = data["source_map"] + if "symbol_map" in data: + evm["symbolMap"] = data["symbol_map"] if any(i + "_runtime" in data for i in evm_keys + pc_maps_keys): evm = output_contracts.setdefault("evm", {}).setdefault("deployedBytecode", {}) @@ -434,6 +438,8 @@ def format_to_output_dict(compiler_data: dict) -> dict: evm["opcodes"] = data["opcodes_runtime"] if "source_map_runtime" in data: evm["sourceMap"] = data["source_map_runtime"] + if "symbol_map_runtime" in data: + evm["symbolMap"] = data["symbol_map_runtime"] if any(i in data for i in VENOM_KEYS): venom = {} diff --git a/vyper/compiler/__init__.py b/vyper/compiler/__init__.py index 00626e4456..f00f83b6bf 100644 --- a/vyper/compiler/__init__.py +++ b/vyper/compiler/__init__.py @@ -46,6 +46,8 @@ "blueprint_bytecode": output.build_blueprint_bytecode_output, "opcodes": output.build_opcodes_output, "opcodes_runtime": output.build_opcodes_runtime_output, + "symbol_map": output.build_symbol_map, + "symbol_map_runtime": output.build_symbol_map_runtime, } INTERFACE_OUTPUT_FORMATS = [ diff --git a/vyper/compiler/output.py b/vyper/compiler/output.py index 18736f7b72..279b7c2cf3 100644 --- a/vyper/compiler/output.py +++ b/vyper/compiler/output.py @@ -10,11 +10,13 @@ from vyper.compiler.phases import CompilerData from vyper.compiler.utils import build_gas_estimates from vyper.evm import opcodes +from vyper.evm.assembler.symbols import resolve_symbols from vyper.exceptions import VyperException from vyper.ir import compile_ir from vyper.semantics.types.function import ContractFunctionT, FunctionVisibility, StateMutability from vyper.typing import StorageLayout from vyper.utils import safe_relpath +from vyper.venom.ir_node_to_venom import _pass_via_stack, _returns_word from vyper.warnings import ContractSizeLimit, vyper_warn @@ -265,6 +267,14 @@ def _to_dict(func_t): ret["source_id"] = func_t.decl_node.module_node.source_id ret["function_id"] = func_t._function_id + if func_t.is_internal and compiler_data.settings.experimental_codegen: + pass_via_stack = _pass_via_stack(func_t) + pass_via_stack_list = [ + arg for (arg, is_stack_arg) in pass_via_stack.items() if is_stack_arg + ] + ret["venom_via_stack"] = pass_via_stack_list + ret["venom_return_via_stack"] = _returns_word(func_t) + keep_keys = { "name", "return_type", @@ -279,6 +289,8 @@ def _to_dict(func_t): "module_path", "source_id", "function_id", + "venom_via_stack", + "venom_return_via_stack", } ret = {k: v for k, v in ret.items() if k in keep_keys} return ret @@ -438,6 +450,16 @@ def _compress_source_map(ast_map, jump_map, bytecode_size): return ";".join(ret) +def build_symbol_map(compiler_data: CompilerData) -> dict[str, int]: + sym, _, _ = resolve_symbols(compiler_data.assembly) + return {k.label: v for (k, v) in sym.items()} + + +def build_symbol_map_runtime(compiler_data: CompilerData) -> dict[str, int]: + sym, _, _ = resolve_symbols(compiler_data.assembly_runtime) + return {k.label: v for (k, v) in sym.items()} + + def build_bytecode_output(compiler_data: CompilerData) -> str: return f"0x{compiler_data.bytecode.hex()}" diff --git a/vyper/venom/ir_node_to_venom.py b/vyper/venom/ir_node_to_venom.py index 6569a77cb4..3e3842540b 100644 --- a/vyper/venom/ir_node_to_venom.py +++ b/vyper/venom/ir_node_to_venom.py @@ -178,7 +178,7 @@ def _append_return_args(fn: IRFunction, ofst: int = 0, size: int = 0): # func_t: ContractFunctionT @functools.lru_cache(maxsize=1024) def _pass_via_stack(func_t) -> dict[str, bool]: - # returns a dict which returns True if a given argument (referered to + # returns a dict which returns True if a given argument (referred to # by name) should be passed via the stack if not ENABLE_NEW_CALL_CONV: return {arg.name: False for arg in func_t.arguments}