Skip to content

Commit 6d0ce5e

Browse files
authored
[mypyc] Add prefix to attributes of generator classes (#19535)
Fixes mypyc/mypyc#1120 `async` functions were recently changed to be represented as generator classes in the IR. Their parameters are represented as attributes in those classes. There are several methods added to every generator class by the compiler, one of those is called `send`. If there is also a parameter called `send`, mypyc assumes that the method is a property getter/setter and inserts method calls in the generated code for `GetAttr` and `SetAttr` nodes in the IR. This is incorrect and led to the compilation error in the linked issue because the `send` method of generator classes takes one more argument than a property getter would. The name clash is fixed by adding a prefix `__mypyc_generator_attribute__` to attribute names derived from parameters, which should ensure that argument references are not converted into method calls.
1 parent a07abb6 commit 6d0ce5e

File tree

7 files changed

+122
-28
lines changed

7 files changed

+122
-28
lines changed

mypyc/codegen/emitfunc.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from mypyc.codegen.cstring import c_string_initializer
99
from mypyc.codegen.emit import DEBUG_ERRORS, Emitter, TracebackAndGotoHandler, c_array_initializer
1010
from mypyc.common import (
11+
GENERATOR_ATTRIBUTE_PREFIX,
1112
HAVE_IMMORTAL,
1213
MODULE_PREFIX,
1314
NATIVE_PREFIX,
@@ -436,7 +437,9 @@ def visit_get_attr(self, op: GetAttr) -> None:
436437
exc_class = "PyExc_AttributeError"
437438
self.emitter.emit_line(
438439
'PyErr_SetString({}, "attribute {} of {} undefined");'.format(
439-
exc_class, repr(op.attr), repr(cl.name)
440+
exc_class,
441+
repr(op.attr.removeprefix(GENERATOR_ATTRIBUTE_PREFIX)),
442+
repr(cl.name),
440443
)
441444
)
442445

@@ -938,7 +941,7 @@ def emit_attribute_error(self, op: Branch, class_name: str, attr: str) -> None:
938941
self.source_path.replace("\\", "\\\\"),
939942
op.traceback_entry[0],
940943
class_name,
941-
attr,
944+
attr.removeprefix(GENERATOR_ATTRIBUTE_PREFIX),
942945
op.traceback_entry[1],
943946
globals_static,
944947
)

mypyc/common.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
LAMBDA_NAME: Final = "__mypyc_lambda__"
2323
PROPSET_PREFIX: Final = "__mypyc_setter__"
2424
SELF_NAME: Final = "__mypyc_self__"
25+
GENERATOR_ATTRIBUTE_PREFIX: Final = "__mypyc_generator_attribute__"
2526

2627
# Max short int we accept as a literal is based on 32-bit platforms,
2728
# so that we can just always emit the same code.

mypyc/irbuild/builder.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
)
6060
from mypy.util import module_prefix, split_target
6161
from mypy.visitor import ExpressionVisitor, StatementVisitor
62-
from mypyc.common import BITMAP_BITS, SELF_NAME, TEMP_ATTR_NAME
62+
from mypyc.common import BITMAP_BITS, GENERATOR_ATTRIBUTE_PREFIX, SELF_NAME, TEMP_ATTR_NAME
6363
from mypyc.crash import catch_errors
6464
from mypyc.errors import Errors
6565
from mypyc.ir.class_ir import ClassIR, NonExtClassInfo
@@ -651,7 +651,11 @@ def get_assignment_target(
651651
# current environment.
652652
if self.fn_info.is_generator:
653653
return self.add_var_to_env_class(
654-
symbol, reg_type, self.fn_info.generator_class, reassign=False
654+
symbol,
655+
reg_type,
656+
self.fn_info.generator_class,
657+
reassign=False,
658+
prefix=GENERATOR_ATTRIBUTE_PREFIX,
655659
)
656660

657661
# Otherwise define a new local variable.
@@ -1333,10 +1337,11 @@ def add_var_to_env_class(
13331337
base: FuncInfo | ImplicitClass,
13341338
reassign: bool = False,
13351339
always_defined: bool = False,
1340+
prefix: str = "",
13361341
) -> AssignmentTarget:
13371342
# First, define the variable name as an attribute of the environment class, and then
13381343
# construct a target for that attribute.
1339-
name = remangle_redefinition_name(var.name)
1344+
name = prefix + remangle_redefinition_name(var.name)
13401345
self.fn_info.env_class.attributes[name] = rtype
13411346
if always_defined:
13421347
self.fn_info.env_class.attrs_with_defaults.add(name)

mypyc/irbuild/env_class.py

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@ def g() -> int:
1818
from __future__ import annotations
1919

2020
from mypy.nodes import Argument, FuncDef, SymbolNode, Var
21-
from mypyc.common import BITMAP_BITS, ENV_ATTR_NAME, SELF_NAME, bitmap_name
21+
from mypyc.common import (
22+
BITMAP_BITS,
23+
ENV_ATTR_NAME,
24+
GENERATOR_ATTRIBUTE_PREFIX,
25+
SELF_NAME,
26+
bitmap_name,
27+
)
2228
from mypyc.ir.class_ir import ClassIR
2329
from mypyc.ir.ops import Call, GetAttr, SetAttr, Value
2430
from mypyc.ir.rtypes import RInstance, bitmap_rprimitive, object_rprimitive
@@ -60,7 +66,7 @@ class is generated, the function environment has not yet been
6066
return env_class
6167

6268

63-
def finalize_env_class(builder: IRBuilder) -> None:
69+
def finalize_env_class(builder: IRBuilder, prefix: str = "") -> None:
6470
"""Generate, instantiate, and set up the environment of an environment class."""
6571
if not builder.fn_info.can_merge_generator_and_env_classes():
6672
instantiate_env_class(builder)
@@ -69,9 +75,9 @@ def finalize_env_class(builder: IRBuilder) -> None:
6975
# that were previously added to the environment with references to the function's
7076
# environment class.
7177
if builder.fn_info.is_nested:
72-
add_args_to_env(builder, local=False, base=builder.fn_info.callable_class)
78+
add_args_to_env(builder, local=False, base=builder.fn_info.callable_class, prefix=prefix)
7379
else:
74-
add_args_to_env(builder, local=False, base=builder.fn_info)
80+
add_args_to_env(builder, local=False, base=builder.fn_info, prefix=prefix)
7581

7682

7783
def instantiate_env_class(builder: IRBuilder) -> Value:
@@ -96,15 +102,15 @@ def instantiate_env_class(builder: IRBuilder) -> Value:
96102
return curr_env_reg
97103

98104

99-
def load_env_registers(builder: IRBuilder) -> None:
105+
def load_env_registers(builder: IRBuilder, prefix: str = "") -> None:
100106
"""Load the registers for the current FuncItem being visited.
101107
102108
Adds the arguments of the FuncItem to the environment. If the
103109
FuncItem is nested inside of another function, then this also
104110
loads all of the outer environments of the FuncItem into registers
105111
so that they can be used when accessing free variables.
106112
"""
107-
add_args_to_env(builder, local=True)
113+
add_args_to_env(builder, local=True, prefix=prefix)
108114

109115
fn_info = builder.fn_info
110116
fitem = fn_info.fitem
@@ -113,7 +119,7 @@ def load_env_registers(builder: IRBuilder) -> None:
113119
# If this is a FuncDef, then make sure to load the FuncDef into its own environment
114120
# class so that the function can be called recursively.
115121
if isinstance(fitem, FuncDef) and fn_info.add_nested_funcs_to_env:
116-
setup_func_for_recursive_call(builder, fitem, fn_info.callable_class)
122+
setup_func_for_recursive_call(builder, fitem, fn_info.callable_class, prefix=prefix)
117123

118124

119125
def load_outer_env(
@@ -134,8 +140,11 @@ def load_outer_env(
134140
assert isinstance(env.type, RInstance), f"{env} must be of type RInstance"
135141

136142
for symbol, target in outer_env.items():
137-
env.type.class_ir.attributes[symbol.name] = target.type
138-
symbol_target = AssignmentTargetAttr(env, symbol.name)
143+
attr_name = symbol.name
144+
if isinstance(target, AssignmentTargetAttr):
145+
attr_name = target.attr
146+
env.type.class_ir.attributes[attr_name] = target.type
147+
symbol_target = AssignmentTargetAttr(env, attr_name)
139148
builder.add_target(symbol, symbol_target)
140149

141150
return env
@@ -178,6 +187,7 @@ def add_args_to_env(
178187
local: bool = True,
179188
base: FuncInfo | ImplicitClass | None = None,
180189
reassign: bool = True,
190+
prefix: str = "",
181191
) -> None:
182192
fn_info = builder.fn_info
183193
args = fn_info.fitem.arguments
@@ -193,10 +203,12 @@ def add_args_to_env(
193203
if is_free_variable(builder, arg.variable) or fn_info.is_generator:
194204
rtype = builder.type_to_rtype(arg.variable.type)
195205
assert base is not None, "base cannot be None for adding nonlocal args"
196-
builder.add_var_to_env_class(arg.variable, rtype, base, reassign=reassign)
206+
builder.add_var_to_env_class(
207+
arg.variable, rtype, base, reassign=reassign, prefix=prefix
208+
)
197209

198210

199-
def add_vars_to_env(builder: IRBuilder) -> None:
211+
def add_vars_to_env(builder: IRBuilder, prefix: str = "") -> None:
200212
"""Add relevant local variables and nested functions to the environment class.
201213
202214
Add all variables and functions that are declared/defined within current
@@ -216,7 +228,9 @@ def add_vars_to_env(builder: IRBuilder) -> None:
216228
for var in sorted(builder.free_variables[builder.fn_info.fitem], key=lambda x: x.name):
217229
if isinstance(var, Var):
218230
rtype = builder.type_to_rtype(var.type)
219-
builder.add_var_to_env_class(var, rtype, env_for_func, reassign=False)
231+
builder.add_var_to_env_class(
232+
var, rtype, env_for_func, reassign=False, prefix=prefix
233+
)
220234

221235
if builder.fn_info.fitem in builder.encapsulating_funcs:
222236
for nested_fn in builder.encapsulating_funcs[builder.fn_info.fitem]:
@@ -226,12 +240,16 @@ def add_vars_to_env(builder: IRBuilder) -> None:
226240
# the same name and signature across conditional blocks
227241
# will generate different callable classes, so the callable
228242
# class that gets instantiated must be generic.
243+
if nested_fn.is_generator:
244+
prefix = GENERATOR_ATTRIBUTE_PREFIX
229245
builder.add_var_to_env_class(
230-
nested_fn, object_rprimitive, env_for_func, reassign=False
246+
nested_fn, object_rprimitive, env_for_func, reassign=False, prefix=prefix
231247
)
232248

233249

234-
def setup_func_for_recursive_call(builder: IRBuilder, fdef: FuncDef, base: ImplicitClass) -> None:
250+
def setup_func_for_recursive_call(
251+
builder: IRBuilder, fdef: FuncDef, base: ImplicitClass, prefix: str = ""
252+
) -> None:
235253
"""Enable calling a nested function (with a callable class) recursively.
236254
237255
Adds the instance of the callable class representing the given
@@ -241,7 +259,8 @@ def setup_func_for_recursive_call(builder: IRBuilder, fdef: FuncDef, base: Impli
241259
"""
242260
# First, set the attribute of the environment class so that GetAttr can be called on it.
243261
prev_env = builder.fn_infos[-2].env_class
244-
prev_env.attributes[fdef.name] = builder.type_to_rtype(fdef.type)
262+
attr_name = prefix + fdef.name
263+
prev_env.attributes[attr_name] = builder.type_to_rtype(fdef.type)
245264

246265
if isinstance(base, GeneratorClass):
247266
# If we are dealing with a generator class, then we need to first get the register
@@ -253,7 +272,7 @@ def setup_func_for_recursive_call(builder: IRBuilder, fdef: FuncDef, base: Impli
253272

254273
# Obtain the instance of the callable class representing the FuncDef, and add it to the
255274
# current environment.
256-
val = builder.add(GetAttr(prev_env_reg, fdef.name, -1))
275+
val = builder.add(GetAttr(prev_env_reg, attr_name, -1))
257276
target = builder.add_local_reg(fdef, object_rprimitive)
258277
builder.assign(target, val, -1)
259278

mypyc/irbuild/generator.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from typing import Callable
1414

1515
from mypy.nodes import ARG_OPT, FuncDef, Var
16-
from mypyc.common import ENV_ATTR_NAME, NEXT_LABEL_ATTR_NAME
16+
from mypyc.common import ENV_ATTR_NAME, GENERATOR_ATTRIBUTE_PREFIX, NEXT_LABEL_ATTR_NAME
1717
from mypyc.ir.class_ir import ClassIR
1818
from mypyc.ir.func_ir import FuncDecl, FuncIR
1919
from mypyc.ir.ops import (
@@ -68,14 +68,14 @@ def gen_generator_func(
6868
) -> tuple[FuncIR, Value | None]:
6969
"""Generate IR for generator function that returns generator object."""
7070
setup_generator_class(builder)
71-
load_env_registers(builder)
71+
load_env_registers(builder, prefix=GENERATOR_ATTRIBUTE_PREFIX)
7272
gen_arg_defaults(builder)
7373
if builder.fn_info.can_merge_generator_and_env_classes():
7474
gen = instantiate_generator_class(builder)
7575
builder.fn_info._curr_env_reg = gen
76-
finalize_env_class(builder)
76+
finalize_env_class(builder, prefix=GENERATOR_ATTRIBUTE_PREFIX)
7777
else:
78-
finalize_env_class(builder)
78+
finalize_env_class(builder, prefix=GENERATOR_ATTRIBUTE_PREFIX)
7979
gen = instantiate_generator_class(builder)
8080
builder.add(Return(gen))
8181

@@ -104,11 +104,13 @@ class that implements the function (each function gets a separate class).
104104
and top_level
105105
and top_level.add_nested_funcs_to_env
106106
):
107-
setup_func_for_recursive_call(builder, fitem, builder.fn_info.generator_class)
107+
setup_func_for_recursive_call(
108+
builder, fitem, builder.fn_info.generator_class, prefix=GENERATOR_ATTRIBUTE_PREFIX
109+
)
108110
create_switch_for_generator_class(builder)
109111
add_raise_exception_blocks_to_generator_class(builder, fitem.line)
110112

111-
add_vars_to_env(builder)
113+
add_vars_to_env(builder, prefix=GENERATOR_ATTRIBUTE_PREFIX)
112114

113115
builder.accept(fitem.body)
114116
builder.maybe_add_implicit_return()
@@ -429,7 +431,9 @@ def setup_env_for_generator_class(builder: IRBuilder) -> None:
429431

430432
# Add arguments from the original generator function to the
431433
# environment of the generator class.
432-
add_args_to_env(builder, local=False, base=cls, reassign=False)
434+
add_args_to_env(
435+
builder, local=False, base=cls, reassign=False, prefix=GENERATOR_ATTRIBUTE_PREFIX
436+
)
433437

434438
# Set the next label register for the generator class.
435439
cls.next_label_reg = builder.read(cls.next_label_target, fitem.line)

mypyc/test-data/run-async.test

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1208,3 +1208,29 @@ async def test_async_context_manager_exception_handling() -> None:
12081208

12091209
[file asyncio/__init__.pyi]
12101210
async def sleep(t: float) -> None: ...
1211+
1212+
[case testCallableArgWithSameNameAsHelperMethod]
1213+
import asyncio
1214+
from typing import Awaitable, Callable
1215+
1216+
1217+
MyCallable = Callable[[int, int], Awaitable[int]]
1218+
1219+
async def add(a: int, b: int) -> int:
1220+
return a + b
1221+
1222+
async def await_send(send: MyCallable) -> int:
1223+
return await send(1, 2)
1224+
1225+
async def await_throw(throw: MyCallable) -> int:
1226+
return await throw(3, 4)
1227+
1228+
async def tests() -> None:
1229+
assert await await_send(add) == 3
1230+
assert await await_throw(add) == 7
1231+
1232+
def test_callable_arg_same_name_as_helper() -> None:
1233+
asyncio.run(tests())
1234+
1235+
[file asyncio/__init__.pyi]
1236+
def run(x: object) -> object: ...

mypyc/test-data/run-generators.test

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,8 +272,17 @@ def call_nested_decorated(x: int) -> list[int]:
272272
a.append(x)
273273
return a
274274

275+
def call_nested_recursive(x: int) -> Iterator:
276+
def recursive(x: int) -> Iterator:
277+
if x > 0:
278+
yield from recursive(x - 1)
279+
yield x
280+
281+
yield from recursive(x)
282+
275283
def test_call_nested_generator_in_function() -> None:
276284
assert call_nested_decorated(5) == [5, 15]
285+
assert list(call_nested_recursive(5)) == [0, 1, 2, 3, 4, 5]
277286

278287
[case testYieldThrow]
279288
from typing import Generator, Iterable, Any, Union
@@ -871,3 +880,30 @@ def test_undefined_int_in_environment() -> None:
871880

872881
with assertRaises(AttributeError): # TODO: Should be UnboundLocalError
873882
list(gen2(False))
883+
884+
[case testVariableWithSameNameAsHelperMethod]
885+
from testutil import assertRaises
886+
from typing import Iterator
887+
888+
def gen_send() -> Iterator[int]:
889+
send = 1
890+
yield send + 1
891+
892+
def gen_throw() -> Iterator[int]:
893+
throw = 42
894+
yield throw * 2
895+
896+
def undefined() -> Iterator[int]:
897+
if int():
898+
send = 1
899+
yield send + 1
900+
901+
def test_same_names() -> None:
902+
assert list(gen_send()) == [2]
903+
assert list(gen_throw()) == [84]
904+
905+
with assertRaises(AttributeError, "attribute 'send' of 'undefined_gen' undefined"):
906+
# TODO: Should be UnboundLocalError, this test verifies that the attribute name
907+
# matches the variable name in the input code, since internally it's generated
908+
# with a prefix.
909+
list(undefined())

0 commit comments

Comments
 (0)