Skip to content

Commit 544b97e

Browse files
authored
[mypyc] Do not emit tracebacks with negative line numbers (#20641)
mypyc may add tracebacks in the generated code with line number -1 when the AST expression that generates an error has no line set. This is usually the case for expressions that are not directly linked with a line in the Python code but are generated internally by mypyc, like helper functions generated for `async` functions. These tracebacks become a problem when running a test that calls a compiled function with pytest. The line number ends up being interpreted by Python as `None` instead of an `int` and pytest crashes when [trying to do math on it](https://github.com/pytest-dev/pytest/blob/main/src/_pytest/_code/code.py#L211). So instead of a stack trace pointing to the compiled function we get an internal pytest error which makes debugging more difficult. Changes in this PR ensure that AST nodes that may produce tracebacks have their line numbers set. For AST nodes generated by mypyc this usually means the line number of the function they are associated with, so an internal attribute access node inside a helper function generated for an `async def my_fun()` will have the line number of `my_fun`. I didn't add a specific test for this because getting an exception from an internally generated node means that there is a bug in mypyc that we should fix but I tested it with one such error that I'm working on and pytest no longer crashes. I have also added assertions so that new changes to mypyc don't cause it to generate tracebacks with negative line numbers. For now the assertions are only enabled in tests since there are cases where mypyc emits tracebacks that we don't have tests for, and the new assertions could break compilation that currently succeeds without them. Once we remove all cases where tracebacks with negative line numbers are possible we can enable them by default.
1 parent fb506ca commit 544b97e

34 files changed

Lines changed: 328 additions & 241 deletions

mypyc/codegen/emit.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ class EmitterContext:
103103
def __init__(
104104
self,
105105
names: NameGenerator,
106+
strict_traceback_checks: bool,
106107
group_name: str | None = None,
107108
group_map: dict[str, str | None] | None = None,
108109
) -> None:
@@ -130,6 +131,8 @@ def __init__(
130131
self.declarations: dict[str, HeaderDeclaration] = {}
131132

132133
self.literals = Literals()
134+
# See mypyc/options.py for context.
135+
self.strict_traceback_checks = strict_traceback_checks
133136

134137

135138
class ErrorHandler:
@@ -1201,6 +1204,8 @@ def _emit_traceback(
12011204
type_str: str = "",
12021205
src: str = "",
12031206
) -> None:
1207+
if self.context.strict_traceback_checks:
1208+
assert traceback_entry[1] >= 0, "Traceback cannot have a negative line number"
12041209
globals_static = self.static_name("globals", module_name)
12051210
line = '%s("%s", "%s", %d, %s' % (
12061211
func,

mypyc/codegen/emitfunc.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -921,6 +921,10 @@ def emit_traceback(self, op: Branch) -> None:
921921

922922
def emit_attribute_error(self, op: Branch, class_name: str, attr: str) -> None:
923923
assert op.traceback_entry is not None
924+
if self.emitter.context.strict_traceback_checks:
925+
assert (
926+
op.traceback_entry[1] >= 0
927+
), "AttributeError traceback cannot have a negative line number"
924928
globals_static = self.emitter.static_name("globals", self.module_name)
925929
self.emit_line(
926930
'CPy_AttributeError("%s", "%s", "%s", "%s", %d, %s);'

mypyc/codegen/emitmodule.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -249,9 +249,9 @@ def compile_scc_to_ir(
249249
for module in modules.values():
250250
for fn in module.functions:
251251
# Insert checks for uninitialized values.
252-
insert_uninit_checks(fn)
252+
insert_uninit_checks(fn, compiler_options.strict_traceback_checks)
253253
# Insert exception handling.
254-
insert_exception_handling(fn)
254+
insert_exception_handling(fn, compiler_options.strict_traceback_checks)
255255
# Insert reference count handling.
256256
insert_ref_count_opcodes(fn)
257257

@@ -535,7 +535,9 @@ def __init__(
535535
"""
536536
self.modules = modules
537537
self.source_paths = source_paths
538-
self.context = EmitterContext(names, group_name, group_map)
538+
self.context = EmitterContext(
539+
names, compiler_options.strict_traceback_checks, group_name, group_map
540+
)
539541
self.names = names
540542
# Initializations of globals to simple values that we can't
541543
# do statically because the windows loader is bad.

mypyc/ir/ops.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -773,9 +773,10 @@ class PrimitiveOp(RegisterOp):
773773
"""
774774

775775
def __init__(self, args: list[Value], desc: PrimitiveDescription, line: int = -1) -> None:
776+
self.error_kind = desc.error_kind
777+
super().__init__(line)
776778
self.args = args
777779
self.type = desc.return_type
778-
self.error_kind = desc.error_kind
779780
self.desc = desc
780781

781782
def sources(self) -> list[Value]:
@@ -849,7 +850,8 @@ class LoadLiteral(RegisterOp):
849850
error_kind = ERR_NEVER
850851
is_borrowed = True
851852

852-
def __init__(self, value: LiteralValue, rtype: RType) -> None:
853+
def __init__(self, value: LiteralValue, rtype: RType, line: int = -1) -> None:
854+
super().__init__(line)
853855
self.value = value
854856
self.type = rtype
855857

@@ -1800,7 +1802,8 @@ class KeepAlive(RegisterOp):
18001802

18011803
error_kind = ERR_NEVER
18021804

1803-
def __init__(self, src: list[Value], *, steal: bool = False) -> None:
1805+
def __init__(self, src: list[Value], line: int = -1, *, steal: bool = False) -> None:
1806+
super().__init__(line)
18041807
assert src
18051808
self.src = src
18061809
self.steal = steal

mypyc/irbuild/ast_helpers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,12 @@ def maybe_process_conditional_comparison(
9090
elif not is_fixed_width_rtype(rtype):
9191
right = self.coerce(right, ltype, e.line)
9292
reg = self.binary_op(left, right, op, e.line)
93-
self.builder.flush_keep_alives()
93+
self.builder.flush_keep_alives(e.line)
9494
self.add_bool_branch(reg, true, false)
9595
else:
9696
# "left op right" for two tagged integers
9797
reg = self.builder.binary_op(left, right, op, e.line)
98-
self.flush_keep_alives()
98+
self.flush_keep_alives(e.line)
9999
self.add_bool_branch(reg, true, false)
100100
return True
101101

mypyc/irbuild/builder.py

Lines changed: 48 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
from mypyc.ir.ops import (
6767
NAMESPACE_MODULE,
6868
NAMESPACE_TYPE_VAR,
69+
NO_TRACEBACK_LINE_NO,
6970
Assign,
7071
BasicBlock,
7172
Branch,
@@ -288,7 +289,7 @@ def accept(self, node: Statement | Expression, *, can_borrow: bool = False) -> V
288289
res = Register(self.node_type(node))
289290
self.can_borrow = old_can_borrow
290291
if not can_borrow:
291-
self.flush_keep_alives()
292+
self.flush_keep_alives(node.line)
292293
return res
293294
else:
294295
try:
@@ -297,8 +298,8 @@ def accept(self, node: Statement | Expression, *, can_borrow: bool = False) -> V
297298
pass
298299
return None
299300

300-
def flush_keep_alives(self) -> None:
301-
self.builder.flush_keep_alives()
301+
def flush_keep_alives(self, line: int) -> None:
302+
self.builder.flush_keep_alives(line)
302303

303304
# Pass through methods for the most common low-level builder ops, for convenience.
304305

@@ -320,23 +321,23 @@ def self(self) -> Register:
320321
def py_get_attr(self, obj: Value, attr: str, line: int) -> Value:
321322
return self.builder.py_get_attr(obj, attr, line)
322323

323-
def load_str(self, value: str) -> Value:
324-
return self.builder.load_str(value)
324+
def load_str(self, value: str, line: int = -1) -> Value:
325+
return self.builder.load_str(value, line)
325326

326-
def load_bytes_from_str_literal(self, value: str) -> Value:
327+
def load_bytes_from_str_literal(self, value: str, line: int = -1) -> Value:
327328
"""Load bytes object from a string literal.
328329
329330
The literal characters of BytesExpr (the characters inside b'')
330331
are stored in BytesExpr.value, whose type is 'str' not 'bytes'.
331332
Thus we perform a special conversion here.
332333
"""
333-
return self.builder.load_bytes(bytes_from_str(value))
334+
return self.builder.load_bytes(bytes_from_str(value), line)
334335

335-
def load_int(self, value: int) -> Value:
336-
return self.builder.load_int(value)
336+
def load_int(self, value: int, line: int = -1) -> Value:
337+
return self.builder.load_int(value, line)
337338

338-
def load_float(self, value: float) -> Value:
339-
return self.builder.load_float(value)
339+
def load_float(self, value: float, line: int = -1) -> Value:
340+
return self.builder.load_float(value, line)
340341

341342
def unary_op(self, lreg: Value, expr_op: str, line: int) -> Value:
342343
return self.builder.unary_op(lreg, expr_op, line)
@@ -347,17 +348,17 @@ def binary_op(self, lreg: Value, rreg: Value, expr_op: str, line: int) -> Value:
347348
def coerce(self, src: Value, target_type: RType, line: int, force: bool = False) -> Value:
348349
return self.builder.coerce(src, target_type, line, force, can_borrow=self.can_borrow)
349350

350-
def none_object(self) -> Value:
351-
return self.builder.none_object()
351+
def none_object(self, line: int = -1) -> Value:
352+
return self.builder.none_object(line)
352353

353-
def none(self) -> Value:
354-
return self.builder.none()
354+
def none(self, line: int = -1) -> Value:
355+
return self.builder.none(line)
355356

356-
def true(self) -> Value:
357-
return self.builder.true()
357+
def true(self, line: int = -1) -> Value:
358+
return self.builder.true(line)
358359

359-
def false(self) -> Value:
360-
return self.builder.false()
360+
def false(self, line: int = -1) -> Value:
361+
return self.builder.false(line)
361362

362363
def new_list_op(self, values: list[Value], line: int) -> Value:
363364
return self.builder.new_list_op(values, line)
@@ -438,7 +439,7 @@ def add_to_non_ext_dict(
438439
self, non_ext: NonExtClassInfo, key: str, val: Value, line: int
439440
) -> None:
440441
# Add an attribute entry into the class dict of a non-extension class.
441-
key_unicode = self.load_str(key)
442+
key_unicode = self.load_str(key, line)
442443
self.primitive_op(dict_set_item_op, [non_ext.dict, key_unicode, val], line)
443444

444445
# It's important that accessing class dictionary items from multiple threads
@@ -452,7 +453,7 @@ def gen_import(self, id: str, line: int) -> None:
452453
self.check_if_module_loaded(id, line, needs_import, out)
453454

454455
self.activate_block(needs_import)
455-
value = self.call_c(import_op, [self.load_str(id)], line)
456+
value = self.call_c(import_op, [self.load_str(id, line)], line)
456457
self.add(InitStatic(value, id, namespace=NAMESPACE_MODULE))
457458
self.goto_and_activate(out)
458459

@@ -467,14 +468,14 @@ def check_if_module_loaded(
467468
needs_import: the BasicBlock that is run if the module has not been loaded yet
468469
out: the BasicBlock that is run if the module has already been loaded"""
469470
first_load = self.load_module(id)
470-
comparison = self.translate_is_op(first_load, self.none_object(), "is not", line)
471+
comparison = self.translate_is_op(first_load, self.none_object(line), "is not", line)
471472
self.add_bool_branch(comparison, out, needs_import)
472473

473474
def get_module(self, module: str, line: int) -> Value:
474475
# Python 3.7 has a nice 'PyImport_GetModule' function that we can't use :(
475476
mod_dict = self.call_c(get_module_dict_op, [], line)
476477
# Get module object from modules dict.
477-
return self.primitive_op(dict_get_item_op, [mod_dict, self.load_str(module)], line)
478+
return self.primitive_op(dict_get_item_op, [mod_dict, self.load_str(module, line)], line)
478479

479480
def get_module_attr(self, module: str, attr: str, line: int) -> Value:
480481
"""Look up an attribute of a module without storing it in the local namespace.
@@ -491,9 +492,9 @@ def get_module_attr(self, module: str, attr: str, line: int) -> Value:
491492
def assign_if_null(self, target: Register, get_val: Callable[[], Value], line: int) -> None:
492493
"""If target is NULL, assign value produced by get_val to it."""
493494
error_block, body_block = BasicBlock(), BasicBlock()
494-
self.add(Branch(target, error_block, body_block, Branch.IS_ERROR))
495+
self.add(Branch(target, error_block, body_block, Branch.IS_ERROR, line))
495496
self.activate_block(error_block)
496-
self.add(Assign(target, self.coerce(get_val(), target.type, line)))
497+
self.add(Assign(target, self.coerce(get_val(), target.type, line), line))
497498
self.goto(body_block)
498499
self.activate_block(body_block)
499500

@@ -508,7 +509,7 @@ def assign_if_bitmap_unset(
508509
IntOp.AND,
509510
line,
510511
)
511-
b = self.add(ComparisonOp(o, Integer(0, bitmap_rprimitive), ComparisonOp.EQ))
512+
b = self.add(ComparisonOp(o, Integer(0, bitmap_rprimitive), ComparisonOp.EQ, line))
512513
self.add(Branch(b, error_block, body_block, Branch.BOOL))
513514
self.activate_block(error_block)
514515
self.add(Assign(target, self.coerce(get_val(), target.type, line)))
@@ -524,8 +525,9 @@ def maybe_add_implicit_return(self) -> None:
524525
def add_implicit_return(self) -> None:
525526
block = self.builder.blocks[-1]
526527
if not block.terminated:
527-
retval = self.coerce(self.builder.none(), self.ret_types[-1], -1)
528-
self.nonlocal_control[-1].gen_return(self, retval, self.fn_info.fitem.line)
528+
line = self.fn_info.fitem.line
529+
retval = self.coerce(self.builder.none(), self.ret_types[-1], line)
530+
self.nonlocal_control[-1].gen_return(self, retval, line)
529531

530532
def add_implicit_unreachable(self) -> None:
531533
block = self.builder.blocks[-1]
@@ -605,13 +607,15 @@ def load_type_var(self, name: str, line: int) -> Value:
605607
)
606608
)
607609

608-
def load_literal_value(self, val: int | str | bytes | float | complex | bool) -> Value:
610+
def load_literal_value(
611+
self, val: int | str | bytes | float | complex | bool, line: int = -1
612+
) -> Value:
609613
"""Load value of a final name, class-level attribute, or constant folded expression."""
610614
if isinstance(val, bool):
611615
if val:
612-
return self.true()
616+
return self.true(line)
613617
else:
614-
return self.false()
618+
return self.false(line)
615619
elif isinstance(val, int):
616620
return self.builder.load_int(val)
617621
elif isinstance(val, float):
@@ -669,7 +673,7 @@ def get_assignment_target(
669673
return self.lookup(symbol)
670674
elif lvalue.kind == GDEF:
671675
globals_dict = self.load_globals_dict()
672-
name = self.load_str(lvalue.name)
676+
name = self.load_str(lvalue.name, line)
673677
return AssignmentTargetIndex(globals_dict, name)
674678
else:
675679
assert False, lvalue.kind
@@ -745,23 +749,23 @@ def read_nullable_attr(self, obj: Value, attr: str, line: int = -1) -> Value:
745749

746750
def assign(self, target: Register | AssignmentTarget, rvalue_reg: Value, line: int) -> None:
747751
if isinstance(target, Register):
748-
self.add(Assign(target, self.coerce_rvalue(rvalue_reg, target.type, line)))
752+
self.add(Assign(target, self.coerce_rvalue(rvalue_reg, target.type, line), line))
749753
elif isinstance(target, AssignmentTargetRegister):
750754
rvalue_reg = self.coerce_rvalue(rvalue_reg, target.type, line)
751-
self.add(Assign(target.register, rvalue_reg))
755+
self.add(Assign(target.register, rvalue_reg, line))
752756
elif isinstance(target, AssignmentTargetAttr):
753757
if isinstance(target.obj_type, RInstance):
754758
setattr = target.obj_type.class_ir.get_method("__setattr__")
755759
if setattr:
756-
key = self.load_str(target.attr)
760+
key = self.load_str(target.attr, line)
757761
boxed_reg = self.builder.box(rvalue_reg)
758762
call = MethodCall(target.obj, setattr.name, [key, boxed_reg], line)
759763
self.add(call)
760764
else:
761765
rvalue_reg = self.coerce_rvalue(rvalue_reg, target.type, line)
762766
self.add(SetAttr(target.obj, target.attr, rvalue_reg, line))
763767
else:
764-
key = self.load_str(target.attr)
768+
key = self.load_str(target.attr, line)
765769
boxed_reg = self.builder.box(rvalue_reg)
766770
self.primitive_op(py_setattr_op, [target.obj, key, boxed_reg], line)
767771
elif isinstance(target, AssignmentTargetIndex):
@@ -931,8 +935,8 @@ def make_spill_target(self, type: RType) -> AssignmentTarget:
931935
def spill(self, value: Value) -> AssignmentTarget:
932936
"""Moves a given Value instance into the generator class' environment class."""
933937
target = self.make_spill_target(value.type)
934-
# Shouldn't be able to fail, so -1 for line
935-
self.assign(target, value, -1)
938+
# Shouldn't be able to fail
939+
self.assign(target, value, NO_TRACEBACK_LINE_NO)
936940
return target
937941

938942
def maybe_spill(self, value: Value) -> Value | AssignmentTarget:
@@ -963,7 +967,7 @@ def maybe_spill_assignable(self, value: Value) -> Register | AssignmentTarget:
963967

964968
# Allocate a temporary register for the assignable value.
965969
reg = Register(value.type)
966-
self.assign(reg, value, -1)
970+
self.assign(reg, value, NO_TRACEBACK_LINE_NO)
967971
return reg
968972

969973
def extract_int(self, e: Expression) -> int | None:
@@ -1132,7 +1136,7 @@ def emit_load_final(
11321136
line: line number where loading occurs
11331137
"""
11341138
if final_var.final_value is not None: # this is safe even for non-native names
1135-
return self.load_literal_value(final_var.final_value)
1139+
return self.load_literal_value(final_var.final_value, line)
11361140
elif native and module_prefix(self.graph, fullname):
11371141
return self.load_final_static(fullname, self.mapper.type_to_rtype(typ), line, name)
11381142
else:
@@ -1409,7 +1413,7 @@ def load_global(self, expr: NameExpr) -> Value:
14091413

14101414
def load_global_str(self, name: str, line: int) -> Value:
14111415
_globals = self.load_globals_dict()
1412-
reg = self.load_str(name)
1416+
reg = self.load_str(name, line)
14131417
return self.primitive_op(dict_get_item_op, [_globals, reg], line)
14141418

14151419
def load_globals_dict(self) -> Value:
@@ -1473,7 +1477,7 @@ def add_coroutine_setup_call(self, class_name: str, obj: Value) -> Value:
14731477
FuncSignature([RuntimeArg("type", object_rprimitive)], bool_rprimitive),
14741478
),
14751479
[obj],
1476-
-1,
1480+
obj.line,
14771481
)
14781482
)
14791483

@@ -1571,13 +1575,13 @@ def create_type_params(
15711575
# To match runtime semantics, pass infer_variance=True
15721576
tv = builder.py_call(
15731577
tvt,
1574-
[builder.load_str(type_param.name), builder.true()],
1578+
[builder.load_str(type_param.name, line), builder.true(line)],
15751579
line,
15761580
arg_kinds=[ARG_POS, ARG_NAMED],
15771581
arg_names=[None, "infer_variance"],
15781582
)
15791583
else:
1580-
tv = builder.py_call(tvt, [builder.load_str(type_param.name)], line)
1584+
tv = builder.py_call(tvt, [builder.load_str(type_param.name, line)], line)
15811585
builder.init_type_var(tv, type_param.name, line)
15821586
tvs.append(tv)
15831587
return tvs

0 commit comments

Comments
 (0)