Skip to content

Commit 1472fc6

Browse files
feat: error map (#2939)
add an error map to source_map output. this makes it easier for downstream tooling to detect why a revert was issued (without having to rely on heuristics or explicit returndata). this also paves the way for brownie-style dev revert strings to be supported by at the language level.
1 parent e3e2f38 commit 1472fc6

File tree

8 files changed

+55
-15
lines changed

8 files changed

+55
-15
lines changed

tests/compiler/test_source_map.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,19 @@ def test_pos_map_offsets():
6868
)
6969

7070

71+
def test_error_map():
72+
code = """
73+
foo: uint256
74+
75+
@external
76+
def update_foo():
77+
self.foo += 1
78+
"""
79+
error_map = compile_code(code, ["source_map"])["source_map"]["error_map"]
80+
assert "safeadd" in list(error_map.values())
81+
assert "fallback function" in list(error_map.values())
82+
83+
7184
def test_compress_source_map():
7285
code = """
7386
@external

vyper/codegen/arithmetic.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,8 @@ def safe_add(x, y):
155155
# TODO push down into optimizer rules.
156156
ok = ["ge", res, x]
157157

158-
ret = IRnode.from_list(["seq", ["assert", ok], res])
158+
check = IRnode.from_list(["assert", ok], error_msg="safeadd")
159+
ret = IRnode.from_list(["seq", check, res])
159160
return b1.resolve(ret)
160161

161162

@@ -184,7 +185,8 @@ def safe_sub(x, y):
184185
# TODO push down into optimizer rules.
185186
ok = ["le", res, x]
186187

187-
ret = IRnode.from_list(["seq", ["assert", ok], res])
188+
check = IRnode.from_list(["assert", ok], error_msg="safesub")
189+
ret = IRnode.from_list(["seq", check, res])
188190
return b1.resolve(ret)
189191

190192

@@ -250,7 +252,8 @@ def safe_mul(x, y):
250252
# (if bits == 256, clamp_basetype is a no-op)
251253
res = clamp_basetype(res)
252254

253-
res = IRnode.from_list(["seq", ["assert", ok], res], typ=res.typ)
255+
check = IRnode.from_list(["assert", ok], error_msg="safediv")
256+
res = IRnode.from_list(["seq", check, res], typ=res.typ)
254257

255258
return b1.resolve(res)
256259

@@ -308,7 +311,7 @@ def safe_div(x, y):
308311
# TODO maybe use safe_mul
309312
res = clamp_basetype(res)
310313

311-
check = ["assert", ok]
314+
check = IRnode.from_list(["assert", ok], error_msg="safemul")
312315
return IRnode.from_list(b1.resolve(["seq", check, res]))
313316

314317

vyper/codegen/core.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -976,7 +976,7 @@ def clamp_basetype(ir_node):
976976
else: # pragma: nocover
977977
raise CompilerPanic(f"{t} passed to clamp_basetype")
978978

979-
return IRnode.from_list(ret, typ=ir_node.typ)
979+
return IRnode.from_list(ret, typ=ir_node.typ, error_msg=f"validate {t}")
980980

981981

982982
def int_clamp(ir_node, bits, signed=False):
@@ -1024,15 +1024,16 @@ def promote_signed_int(x, bits):
10241024
# general clamp function for all ops and numbers
10251025
def clamp(op, arg, bound):
10261026
with IRnode.from_list(arg).cache_when_complex("clamp_arg") as (b1, arg):
1027-
assertion = ["assert", [op, arg, bound]]
1028-
ret = ["seq", assertion, arg]
1027+
check = IRnode.from_list(["assert", [op, arg, bound]], error_msg=f"clamp {op} {bound}")
1028+
ret = ["seq", check, arg]
10291029
return IRnode.from_list(b1.resolve(ret), typ=arg.typ)
10301030

10311031

10321032
def clamp_nonzero(arg):
10331033
# TODO: use clamp("ne", arg, 0) once optimizer rules can handle it
10341034
with IRnode.from_list(arg).cache_when_complex("should_nonzero") as (b1, arg):
1035-
ret = ["seq", ["assert", arg], arg]
1035+
check = IRnode.from_list(["assert", arg], error_msg="clamp_nonzero")
1036+
ret = ["seq", check, arg]
10361037
return IRnode.from_list(b1.resolve(ret), typ=arg.typ)
10371038

10381039

vyper/codegen/ir_node.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ def __init__(
115115
location: Optional[AddrSpace] = None,
116116
source_pos: Optional[Tuple[int, int]] = None,
117117
annotation: Optional[str] = None,
118+
error_msg: Optional[str] = None,
118119
mutable: bool = True,
119120
add_gas_estimate: int = 0,
120121
encoding: Encoding = Encoding.VYPER,
@@ -129,6 +130,7 @@ def __init__(
129130
self.typ = typ
130131
self.location = location
131132
self.source_pos = source_pos
133+
self.error_msg = error_msg
132134
self.annotation = annotation
133135
self.mutable = mutable
134136
self.add_gas_estimate = add_gas_estimate
@@ -494,6 +496,7 @@ def from_list(
494496
location: Optional[AddrSpace] = None,
495497
source_pos: Optional[Tuple[int, int]] = None,
496498
annotation: Optional[str] = None,
499+
error_msg: Optional[str] = None,
497500
mutable: bool = True,
498501
add_gas_estimate: int = 0,
499502
encoding: Encoding = Encoding.VYPER,
@@ -512,6 +515,8 @@ def from_list(
512515
obj.location = location
513516
if obj.encoding is None:
514517
obj.encoding = encoding
518+
if obj.error_msg is None:
519+
obj.error_msg = error_msg
515520

516521
return obj
517522
elif not isinstance(obj, list):
@@ -523,7 +528,9 @@ def from_list(
523528
annotation=annotation,
524529
mutable=mutable,
525530
add_gas_estimate=add_gas_estimate,
531+
source_pos=source_pos,
526532
encoding=encoding,
533+
error_msg=error_msg,
527534
)
528535
else:
529536
return cls(
@@ -536,4 +543,5 @@ def from_list(
536543
source_pos=source_pos,
537544
add_gas_estimate=add_gas_estimate,
538545
encoding=encoding,
546+
error_msg=error_msg,
539547
)

vyper/codegen/module.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,9 @@ def _runtime_ir(runtime_functions, all_sigs, global_ctx):
150150
default_function, all_sigs, global_ctx, skip_nonpayable_check
151151
)
152152
else:
153-
fallback_ir = IRnode.from_list(["revert", 0, 0], annotation="Default function")
153+
fallback_ir = IRnode.from_list(
154+
["revert", 0, 0], annotation="Default function", error_msg="fallback function"
155+
)
154156

155157
# ensure the external jumptable section gets closed out
156158
# (for basic block hygiene and also for zksync interpreter)

vyper/codegen/stmt.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,9 @@ def parse_Call(self):
160160

161161
def _assert_reason(self, test_expr, msg):
162162
if isinstance(msg, vy_ast.Name) and msg.id == "UNREACHABLE":
163-
return IRnode.from_list(["assert_unreachable", test_expr])
163+
return IRnode.from_list(
164+
["assert_unreachable", test_expr], error_msg="assert unreachable"
165+
)
164166

165167
# set constant so that revert reason str is well behaved
166168
try:
@@ -209,21 +211,21 @@ def _get_last(ir):
209211
else:
210212
ir_node = revert_seq
211213

212-
return IRnode.from_list(ir_node)
214+
return IRnode.from_list(ir_node, error_msg="user revert with reason")
213215

214216
def parse_Assert(self):
215217
test_expr = Expr.parse_value_expr(self.stmt.test, self.context)
216218

217219
if self.stmt.msg:
218220
return self._assert_reason(test_expr, self.stmt.msg)
219221
else:
220-
return IRnode.from_list(["assert", test_expr])
222+
return IRnode.from_list(["assert", test_expr], error_msg="user assert")
221223

222224
def parse_Raise(self):
223225
if self.stmt.exc:
224226
return self._assert_reason(None, self.stmt.exc)
225227
else:
226-
return IRnode.from_list(["revert", 0, 0])
228+
return IRnode.from_list(["revert", 0, 0], error_msg="user raise")
227229

228230
def _check_valid_range_constant(self, arg_ast_node):
229231
with self.context.range_scope():

vyper/ir/compile_ir.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,10 @@ class Instruction(str):
164164
def __new__(cls, sstr, *args, **kwargs):
165165
return super().__new__(cls, sstr)
166166

167-
def __init__(self, sstr, source_pos=None):
167+
def __init__(self, sstr, source_pos=None, error_msg=None):
168+
self.error_msg = error_msg
168169
self.pc_debugger = False
170+
169171
if source_pos is not None:
170172
self.lineno, self.col_offset, self.end_lineno, self.end_col_offset = source_pos
171173
else:
@@ -177,8 +179,9 @@ def apply_line_numbers(func):
177179
def apply_line_no_wrapper(*args, **kwargs):
178180
code = args[0]
179181
ret = func(*args, **kwargs)
182+
180183
new_ret = [
181-
Instruction(i, code.source_pos)
184+
Instruction(i, code.source_pos, code.error_msg)
182185
if isinstance(i, str) and not isinstance(i, Instruction)
183186
else i
184187
for i in ret
@@ -732,7 +735,12 @@ def note_line_num(line_number_map, item, pos):
732735
offsets = (item.lineno, item.col_offset, item.end_lineno, item.end_col_offset)
733736
else:
734737
offsets = None
738+
735739
line_number_map["pc_pos_map"][pos] = offsets
740+
741+
if item.error_msg is not None:
742+
line_number_map["error_map"][pos] = item.error_msg
743+
736744
added_line_breakpoint = note_breakpoint(line_number_map, item, pos)
737745
return added_line_breakpoint
738746

@@ -944,6 +952,7 @@ def assembly_to_evm(assembly, start_pos=0, insert_vyper_signature=False):
944952
"pc_breakpoints": set(),
945953
"pc_jump_map": {0: "-"},
946954
"pc_pos_map": {},
955+
"error_map": {},
947956
}
948957

949958
posmap = {}

vyper/ir/optimizer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,7 @@ def _optimize(node: IRnode, parent: Optional[IRnode]) -> Tuple[bool, IRnode]:
424424
typ = node.typ
425425
location = node.location
426426
source_pos = node.source_pos
427+
error_msg = node.error_msg
427428
annotation = node.annotation
428429
add_gas_estimate = node.add_gas_estimate
429430

@@ -445,6 +446,7 @@ def finalize(val, args):
445446
typ=typ,
446447
location=location,
447448
source_pos=source_pos,
449+
error_msg=error_msg,
448450
annotation=annotation,
449451
add_gas_estimate=add_gas_estimate,
450452
)

0 commit comments

Comments
 (0)