33import re
44import subprocess
55import tempfile
6- from typing import Tuple
6+ from functools import cached_property
7+ from typing import Any
78
89from eth_abi import encode
910from eth_utils import function_signature_to_4byte_selector
10- from pydantic import BaseModel , Field
1111from pydantic .functional_validators import BeforeValidator
1212from typing_extensions import Annotated
1313
14- from ethereum_test_base_types import Address , Bytes , Hash , HexNumber
14+ from ethereum_test_base_types import Address , Hash , HexNumber
1515
1616from .compile_yul import compile_yul
1717
@@ -30,12 +30,6 @@ def parse_hex_number(i: str | int) -> int:
3030 return int (i , 10 )
3131
3232
33- class CodeOptions (BaseModel ):
34- """Define options of the code."""
35-
36- label : str = Field ("" )
37-
38-
3933def parse_args_from_string_into_array (stream : str , pos : int , delim : str = " " ):
4034 """Parse YUL options into array."""
4135 args = []
@@ -53,116 +47,146 @@ def parse_args_from_string_into_array(stream: str, pos: int, delim: str = " "):
5347 return args , pos
5448
5549
56- def parse_code (code : str ) -> Tuple [bytes , CodeOptions ]:
57- """Check if the given string is a valid code."""
58- # print("parse `" + str(code) + "`")
59- if isinstance (code , int ):
60- # Users pass code as int (very bad)
61- hex_str = format (code , "02x" )
62- return (bytes .fromhex (hex_str ), CodeOptions ())
63- if not isinstance (code , str ):
64- raise ValueError (f"parse_code(code: str) code is not string: { code } " )
65- if len (code ) == 0 :
66- return (bytes .fromhex ("" ), CodeOptions ())
67-
68- compiled_code = ""
69- code_options : CodeOptions = CodeOptions ()
70-
71- raw_marker = ":raw 0x"
72- raw_index = code .find (raw_marker )
73- abi_marker = ":abi"
74- abi_index = code .find (abi_marker )
50+ class CodeInFillerSource :
51+ """Not compiled code source in test filler."""
52+
53+ code_label : str | None
54+ code_raw : Any
55+
56+ def __init__ (self , code : Any , label : str | None = None ):
57+ """Instantiate."""
58+ self .code_label = label
59+ self .code_raw = code
60+
61+ @cached_property
62+ def compiled (self ) -> bytes :
63+ """Compile the code from source to bytes."""
64+ if isinstance (self .code_raw , int ):
65+ # Users pass code as int (very bad)
66+ hex_str = format (self .code_raw , "02x" )
67+ return bytes .fromhex (hex_str )
68+
69+ if not isinstance (self .code_raw , str ):
70+ raise ValueError (f"parse_code(code: str) code is not string: { self .code_raw } " )
71+ if len (self .code_raw ) == 0 :
72+ return b""
73+
74+ compiled_code = ""
75+
76+ raw_marker = ":raw 0x"
77+ raw_index = self .code_raw .find (raw_marker )
78+ abi_marker = ":abi"
79+ abi_index = self .code_raw .find (abi_marker )
80+ yul_marker = ":yul"
81+ yul_index = self .code_raw .find (yul_marker )
82+
83+ # Parse :raw
84+ if raw_index != - 1 :
85+ compiled_code = self .code_raw [raw_index + len (raw_marker ) :]
86+
87+ # Parse :yul
88+ elif yul_index != - 1 :
89+ option_start = yul_index + len (yul_marker )
90+ options : list [str ] = []
91+ native_yul_options : str = ""
92+
93+ if self .code_raw [option_start :].lstrip ().startswith ("{" ):
94+ # No yul options, proceed to code parsing
95+ source_start = option_start
96+ else :
97+ opt , source_start = parse_args_from_string_into_array (
98+ self .code_raw , option_start + 1
99+ )
100+ for arg in opt :
101+ if arg == "object" or arg == '"C"' :
102+ native_yul_options += arg + " "
103+ else :
104+ options .append (arg )
105+
106+ with tempfile .NamedTemporaryFile (mode = "w+" , delete = False ) as tmp :
107+ tmp .write (native_yul_options + self .code_raw [source_start :])
108+ tmp_path = tmp .name
109+ compiled_code = compile_yul (
110+ source_file = tmp_path ,
111+ evm_version = options [0 ] if len (options ) >= 1 else None ,
112+ optimize = options [1 ] if len (options ) >= 2 else None ,
113+ )[2 :]
114+
115+ # Parse :abi
116+ elif abi_index != - 1 :
117+ abi_encoding = self .code_raw [abi_index + len (abi_marker ) + 1 :]
118+ tokens = abi_encoding .strip ().split ()
119+ abi = tokens [0 ]
120+ function_signature = function_signature_to_4byte_selector (abi )
121+ parameter_str = re .sub (r"^\w+" , "" , abi ).strip ()
122+
123+ parameter_types = parameter_str .strip ("()" ).split ("," )
124+ if len (tokens ) > 1 :
125+ function_parameters = encode (
126+ [parameter_str ],
127+ [
128+ [
129+ int (t .lower (), 0 ) & ((1 << 256 ) - 1 ) # treat big ints as 256bits
130+ if parameter_types [t_index ] == "uint"
131+ else int (t .lower (), 0 ) > 0 # treat positive values as True
132+ if parameter_types [t_index ] == "bool"
133+ else False and ValueError ("unhandled parameter_types" )
134+ for t_index , t in enumerate (tokens [1 :])
135+ ]
136+ ],
137+ )
138+ return function_signature + function_parameters
139+ return function_signature
140+
141+ # Parse plain code 0x
142+ elif self .code_raw .lstrip ().startswith ("0x" ):
143+ compiled_code = self .code_raw [2 :].lower ()
144+
145+ # Parse lllc code
146+ elif self .code_raw .lstrip ().startswith ("{" ) or self .code_raw .lstrip ().startswith ("(asm" ):
147+ with tempfile .NamedTemporaryFile (mode = "w+" , delete = False ) as tmp :
148+ tmp .write (self .code_raw )
149+ tmp_path = tmp .name
150+
151+ # - using lllc
152+ result = subprocess .run (["lllc" , tmp_path ], capture_output = True , text = True )
153+
154+ # - using docker:
155+ # If the running machine does not have lllc installed, we can use docker to run lllc,
156+ # but we need to start a container first, and the process is generally slower.
157+ # from .docker import get_lllc_container_id
158+ # result = subprocess.run(
159+ # ["docker", "exec", get_lllc_container_id(), "lllc", tmp_path[5:]],
160+ # capture_output=True,
161+ # text=True,
162+ # )
163+ compiled_code = "" .join (result .stdout .splitlines ())
164+ else :
165+ raise Exception (f'Error parsing code: "{ self .code_raw } "' )
166+
167+ try :
168+ return bytes .fromhex (compiled_code )
169+ except ValueError as e :
170+ raise Exception (f'Error parsing compile code: "{ self .code_raw } "' ) from e
171+
172+
173+ def parse_code_label (code ) -> CodeInFillerSource :
174+ """Parse label from code."""
75175 label_marker = ":label"
76176 label_index = code .find (label_marker )
77- yul_marker = ":yul"
78- yul_index = code .find (yul_marker )
79177
80178 # Parse :label into code options
179+ label = None
81180 if label_index != - 1 :
82181 space_index = code .find (" " , label_index + len (label_marker ) + 1 )
83182 if space_index == - 1 :
84183 label = code [label_index + len (label_marker ) + 1 :]
85184 else :
86185 label = code [label_index + len (label_marker ) + 1 : space_index ]
87- code_options .label = label
88-
89- # Prase :raw
90- if raw_index != - 1 :
91- compiled_code = code [raw_index + len (raw_marker ) :]
92-
93- elif yul_index != - 1 :
94- option_start = yul_index + len (yul_marker )
95- options : list [str ] = []
96- native_yul_options : str = ""
97-
98- if code [option_start :].lstrip ().startswith ("{" ):
99- # No yul options, proceed to code parsing
100- source_start = option_start
101- else :
102- opt , source_start = parse_args_from_string_into_array (code , option_start + 1 )
103- for arg in opt :
104- if arg == "object" or arg == '"C"' :
105- native_yul_options += arg + " "
106- else :
107- options .append (arg )
108-
109- with tempfile .NamedTemporaryFile (mode = "w+" , delete = False ) as tmp :
110- tmp .write (native_yul_options + code [source_start :])
111- tmp_path = tmp .name
112- compiled_code = compile_yul (
113- source_file = tmp_path ,
114- evm_version = options [0 ] if len (options ) >= 1 else None ,
115- optimize = options [1 ] if len (options ) >= 2 else None ,
116- )[2 :]
117-
118- # Prase :abi
119- elif abi_index != - 1 :
120- abi_encoding = code [abi_index + len (abi_marker ) + 1 :]
121- tokens = abi_encoding .strip ().split ()
122- abi = tokens [0 ]
123- function_signature = function_signature_to_4byte_selector (abi )
124- parameter_str = re .sub (r"^\w+" , "" , abi ).strip ()
125-
126- parameter_types = parameter_str .strip ("()" ).split ("," )
127- if len (tokens ) > 1 :
128- function_parameters = encode (
129- [parameter_str ],
130- [
131- [
132- int (t .lower (), 0 ) & ((1 << 256 ) - 1 ) # treat big ints as 256bits
133- if parameter_types [t_index ] == "uint"
134- else int (t .lower (), 0 ) > 0 # treat positive values as True
135- if parameter_types [t_index ] == "bool"
136- else False and ValueError ("unhandled parameter_types" )
137- for t_index , t in enumerate (tokens [1 :])
138- ]
139- ],
140- )
141- return (function_signature + function_parameters , code_options )
142- return (function_signature , code_options )
143-
144- # Prase plain code 0x
145- elif code .lstrip ().startswith ("0x" ):
146- compiled_code = code [2 :].lower ()
147-
148- # Prase lllc code
149- elif code .lstrip ().startswith ("{" ) or code .lstrip ().startswith ("(asm" ):
150- binary_path = "lllc"
151- with tempfile .NamedTemporaryFile (mode = "w+" , delete = False ) as tmp :
152- tmp .write (code )
153- tmp_path = tmp .name
154- result = subprocess .run ([binary_path , tmp_path ], capture_output = True , text = True )
155- compiled_code = "" .join (result .stdout .splitlines ())
156- else :
157- raise Exception (f'Error parsing code: "{ code } "' )
158-
159- try :
160- return (bytes .fromhex (compiled_code ), code_options )
161- except ValueError as e :
162- raise Exception (f'Error parsing compile code: "{ code } "' ) from e
186+ return CodeInFillerSource (code , label )
163187
164188
165189AddressInFiller = Annotated [Address , BeforeValidator (lambda a : Address (a , left_padding = True ))]
166190ValueInFiller = Annotated [HexNumber , BeforeValidator (parse_hex_number )]
167- CodeInFiller = Annotated [Tuple [ Bytes , CodeOptions ], BeforeValidator (parse_code )]
191+ CodeInFiller = Annotated [CodeInFillerSource , BeforeValidator (parse_code_label )]
168192Hash32InFiller = Annotated [Hash , BeforeValidator (lambda h : Hash (h , left_padding = True ))]
0 commit comments