Skip to content

Commit c0218a4

Browse files
authored
[mypyc] Refactor IR building for generator functions (#19008)
The code was pretty hard to follow, since there were many conditional code paths in a very long function that handles both normal functions, nested functions and generators. Move much of the generator-specific code to a helper function. This required moving some functionality to helper functions to avoid code duplication. Also pass a function as an argument to avoid a dependency cycle. This is quite a big refactoring, and it's easier to follow by looking at the individual commits in this PR.
1 parent 43e8130 commit c0218a4

File tree

4 files changed

+174
-136
lines changed

4 files changed

+174
-136
lines changed

mypyc/irbuild/builder.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1169,7 +1169,7 @@ def flatten_classes(self, arg: RefExpr | TupleExpr) -> list[ClassIR] | None:
11691169
return None
11701170
return res
11711171

1172-
def enter(self, fn_info: FuncInfo | str = "") -> None:
1172+
def enter(self, fn_info: FuncInfo | str = "", *, ret_type: RType = none_rprimitive) -> None:
11731173
if isinstance(fn_info, str):
11741174
fn_info = FuncInfo(name=fn_info)
11751175
self.builder = LowLevelIRBuilder(self.errors, self.options)
@@ -1179,7 +1179,7 @@ def enter(self, fn_info: FuncInfo | str = "") -> None:
11791179
self.runtime_args.append([])
11801180
self.fn_info = fn_info
11811181
self.fn_infos.append(self.fn_info)
1182-
self.ret_types.append(none_rprimitive)
1182+
self.ret_types.append(ret_type)
11831183
if fn_info.is_generator:
11841184
self.nonlocal_control.append(GeneratorNonlocalControl())
11851185
else:
@@ -1219,10 +1219,9 @@ def enter_method(
12191219
self_type: If not None, override default type of the implicit 'self'
12201220
argument (by default, derive type from class_ir)
12211221
"""
1222-
self.enter(fn_info)
1222+
self.enter(fn_info, ret_type=ret_type)
12231223
self.function_name_stack.append(name)
12241224
self.class_ir_stack.append(class_ir)
1225-
self.ret_types[-1] = ret_type
12261225
if self_type is None:
12271226
self_type = RInstance(class_ir)
12281227
self.add_argument(SELF_NAME, self_type)
@@ -1498,3 +1497,30 @@ def create_type_params(
14981497
builder.init_type_var(tv, type_param.name, line)
14991498
tvs.append(tv)
15001499
return tvs
1500+
1501+
1502+
def calculate_arg_defaults(
1503+
builder: IRBuilder,
1504+
fn_info: FuncInfo,
1505+
func_reg: Value | None,
1506+
symtable: dict[SymbolNode, SymbolTarget],
1507+
) -> None:
1508+
"""Calculate default argument values and store them.
1509+
1510+
They are stored in statics for top level functions and in
1511+
the function objects for nested functions (while constants are
1512+
still stored computed on demand).
1513+
"""
1514+
fitem = fn_info.fitem
1515+
for arg in fitem.arguments:
1516+
# Constant values don't get stored but just recomputed
1517+
if arg.initializer and not is_constant(arg.initializer):
1518+
value = builder.coerce(
1519+
builder.accept(arg.initializer), symtable[arg.variable].type, arg.line
1520+
)
1521+
if not fn_info.is_nested:
1522+
name = fitem.fullname + "." + arg.variable.name
1523+
builder.add(InitStatic(value, name, builder.module_name))
1524+
else:
1525+
assert func_reg is not None
1526+
builder.add(SetAttr(func_reg, arg.variable.name, value, arg.line))

mypyc/irbuild/env_class.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,41 @@ def add_args_to_env(
191191
builder.add_var_to_env_class(arg.variable, rtype, base, reassign=reassign)
192192

193193

194+
def add_vars_to_env(builder: IRBuilder) -> None:
195+
"""Add relevant local variables and nested functions to the environment class.
196+
197+
Add all variables and functions that are declared/defined within current
198+
function and are referenced in functions nested within this one to this
199+
function's environment class so the nested functions can reference
200+
them even if they are declared after the nested function's definition.
201+
Note that this is done before visiting the body of the function.
202+
"""
203+
env_for_func: FuncInfo | ImplicitClass = builder.fn_info
204+
if builder.fn_info.is_generator:
205+
env_for_func = builder.fn_info.generator_class
206+
elif builder.fn_info.is_nested or builder.fn_info.in_non_ext:
207+
env_for_func = builder.fn_info.callable_class
208+
209+
if builder.fn_info.fitem in builder.free_variables:
210+
# Sort the variables to keep things deterministic
211+
for var in sorted(builder.free_variables[builder.fn_info.fitem], key=lambda x: x.name):
212+
if isinstance(var, Var):
213+
rtype = builder.type_to_rtype(var.type)
214+
builder.add_var_to_env_class(var, rtype, env_for_func, reassign=False)
215+
216+
if builder.fn_info.fitem in builder.encapsulating_funcs:
217+
for nested_fn in builder.encapsulating_funcs[builder.fn_info.fitem]:
218+
if isinstance(nested_fn, FuncDef):
219+
# The return type is 'object' instead of an RInstance of the
220+
# callable class because differently defined functions with
221+
# the same name and signature across conditional blocks
222+
# will generate different callable classes, so the callable
223+
# class that gets instantiated must be generic.
224+
builder.add_var_to_env_class(
225+
nested_fn, object_rprimitive, env_for_func, reassign=False
226+
)
227+
228+
194229
def setup_func_for_recursive_call(builder: IRBuilder, fdef: FuncDef, base: ImplicitClass) -> None:
195230
"""Enable calling a nested function (with a callable class) recursively.
196231

mypyc/irbuild/function.py

Lines changed: 45 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
FuncItem,
2626
LambdaExpr,
2727
OverloadedFuncDef,
28-
SymbolNode,
2928
TypeInfo,
3029
Var,
3130
)
@@ -44,7 +43,6 @@
4443
from mypyc.ir.ops import (
4544
BasicBlock,
4645
GetAttr,
47-
InitStatic,
4846
Integer,
4947
LoadAddress,
5048
LoadLiteral,
@@ -62,31 +60,22 @@
6260
int_rprimitive,
6361
object_rprimitive,
6462
)
65-
from mypyc.irbuild.builder import IRBuilder, SymbolTarget, gen_arg_defaults
63+
from mypyc.irbuild.builder import IRBuilder, calculate_arg_defaults, gen_arg_defaults
6664
from mypyc.irbuild.callable_class import (
6765
add_call_to_callable_class,
6866
add_get_to_callable_class,
6967
instantiate_callable_class,
7068
setup_callable_class,
7169
)
72-
from mypyc.irbuild.context import FuncInfo, ImplicitClass
70+
from mypyc.irbuild.context import FuncInfo
7371
from mypyc.irbuild.env_class import (
72+
add_vars_to_env,
7473
finalize_env_class,
7574
load_env_registers,
76-
load_outer_envs,
7775
setup_env_class,
78-
setup_func_for_recursive_call,
79-
)
80-
from mypyc.irbuild.generator import (
81-
add_methods_to_generator_class,
82-
add_raise_exception_blocks_to_generator_class,
83-
create_switch_for_generator_class,
84-
gen_generator_func,
85-
populate_switch_for_generator_class,
86-
setup_env_for_generator_class,
8776
)
77+
from mypyc.irbuild.generator import gen_generator_func, gen_generator_func_body
8878
from mypyc.irbuild.targets import AssignmentTarget
89-
from mypyc.irbuild.util import is_constant
9079
from mypyc.primitives.dict_ops import dict_get_method_with_none, dict_new_op, dict_set_item_op
9180
from mypyc.primitives.generic_ops import py_setattr_op
9281
from mypyc.primitives.misc_ops import register_function
@@ -235,123 +224,77 @@ def c() -> None:
235224
func_name = singledispatch_main_func_name(name)
236225
else:
237226
func_name = name
238-
builder.enter(
239-
FuncInfo(
240-
fitem=fitem,
241-
name=func_name,
242-
class_name=class_name,
243-
namespace=gen_func_ns(builder),
244-
is_nested=is_nested,
245-
contains_nested=contains_nested,
246-
is_decorated=is_decorated,
247-
in_non_ext=in_non_ext,
248-
add_nested_funcs_to_env=add_nested_funcs_to_env,
249-
)
227+
228+
fn_info = FuncInfo(
229+
fitem=fitem,
230+
name=func_name,
231+
class_name=class_name,
232+
namespace=gen_func_ns(builder),
233+
is_nested=is_nested,
234+
contains_nested=contains_nested,
235+
is_decorated=is_decorated,
236+
in_non_ext=in_non_ext,
237+
add_nested_funcs_to_env=add_nested_funcs_to_env,
250238
)
239+
is_generator = fn_info.is_generator
240+
builder.enter(fn_info, ret_type=sig.ret_type)
251241

252242
# Functions that contain nested functions need an environment class to store variables that
253243
# are free in their nested functions. Generator functions need an environment class to
254244
# store a variable denoting the next instruction to be executed when the __next__ function
255245
# is called, along with all the variables inside the function itself.
256-
if builder.fn_info.contains_nested or builder.fn_info.is_generator:
246+
if contains_nested or is_generator:
257247
setup_env_class(builder)
258248

259-
if builder.fn_info.is_nested or builder.fn_info.in_non_ext:
249+
if is_nested or in_non_ext:
260250
setup_callable_class(builder)
261251

262-
if builder.fn_info.is_generator:
263-
# Do a first-pass and generate a function that just returns a generator object.
264-
gen_generator_func(builder)
265-
args, _, blocks, ret_type, fn_info = builder.leave()
266-
func_ir, func_reg = gen_func_ir(
267-
builder, args, blocks, sig, fn_info, cdef, is_singledispatch
252+
if is_generator:
253+
# First generate a function that just constructs and returns a generator object.
254+
func_ir, func_reg = gen_generator_func(
255+
builder,
256+
lambda args, blocks, fn_info: gen_func_ir(
257+
builder, args, blocks, sig, fn_info, cdef, is_singledispatch
258+
),
268259
)
269260

270261
# Re-enter the FuncItem and visit the body of the function this time.
271-
builder.enter(fn_info)
272-
setup_env_for_generator_class(builder)
273-
274-
load_outer_envs(builder, builder.fn_info.generator_class)
275-
top_level = builder.top_level_fn_info()
276-
if (
277-
builder.fn_info.is_nested
278-
and isinstance(fitem, FuncDef)
279-
and top_level
280-
and top_level.add_nested_funcs_to_env
281-
):
282-
setup_func_for_recursive_call(builder, fitem, builder.fn_info.generator_class)
283-
create_switch_for_generator_class(builder)
284-
add_raise_exception_blocks_to_generator_class(builder, fitem.line)
262+
gen_generator_func_body(builder, fn_info, sig, func_reg)
285263
else:
286-
load_env_registers(builder)
287-
gen_arg_defaults(builder)
264+
func_ir, func_reg = gen_func_body(builder, sig, cdef, is_singledispatch)
288265

289-
if builder.fn_info.contains_nested and not builder.fn_info.is_generator:
290-
finalize_env_class(builder)
266+
if is_singledispatch:
267+
# add the generated main singledispatch function
268+
builder.functions.append(func_ir)
269+
# create the dispatch function
270+
assert isinstance(fitem, FuncDef)
271+
return gen_dispatch_func_ir(builder, fitem, fn_info.name, name, sig)
291272

292-
builder.ret_types[-1] = sig.ret_type
273+
return func_ir, func_reg
293274

294-
# Add all variables and functions that are declared/defined within this
295-
# function and are referenced in functions nested within this one to this
296-
# function's environment class so the nested functions can reference
297-
# them even if they are declared after the nested function's definition.
298-
# Note that this is done before visiting the body of this function.
299-
300-
env_for_func: FuncInfo | ImplicitClass = builder.fn_info
301-
if builder.fn_info.is_generator:
302-
env_for_func = builder.fn_info.generator_class
303-
elif builder.fn_info.is_nested or builder.fn_info.in_non_ext:
304-
env_for_func = builder.fn_info.callable_class
305-
306-
if builder.fn_info.fitem in builder.free_variables:
307-
# Sort the variables to keep things deterministic
308-
for var in sorted(builder.free_variables[builder.fn_info.fitem], key=lambda x: x.name):
309-
if isinstance(var, Var):
310-
rtype = builder.type_to_rtype(var.type)
311-
builder.add_var_to_env_class(var, rtype, env_for_func, reassign=False)
312-
313-
if builder.fn_info.fitem in builder.encapsulating_funcs:
314-
for nested_fn in builder.encapsulating_funcs[builder.fn_info.fitem]:
315-
if isinstance(nested_fn, FuncDef):
316-
# The return type is 'object' instead of an RInstance of the
317-
# callable class because differently defined functions with
318-
# the same name and signature across conditional blocks
319-
# will generate different callable classes, so the callable
320-
# class that gets instantiated must be generic.
321-
builder.add_var_to_env_class(
322-
nested_fn, object_rprimitive, env_for_func, reassign=False
323-
)
324275

325-
builder.accept(fitem.body)
276+
def gen_func_body(
277+
builder: IRBuilder, sig: FuncSignature, cdef: ClassDef | None, is_singledispatch: bool
278+
) -> tuple[FuncIR, Value | None]:
279+
load_env_registers(builder)
280+
gen_arg_defaults(builder)
281+
if builder.fn_info.contains_nested:
282+
finalize_env_class(builder)
283+
add_vars_to_env(builder)
284+
builder.accept(builder.fn_info.fitem.body)
326285
builder.maybe_add_implicit_return()
327286

328-
if builder.fn_info.is_generator:
329-
populate_switch_for_generator_class(builder)
330-
331287
# Hang on to the local symbol table for a while, since we use it
332288
# to calculate argument defaults below.
333289
symtable = builder.symtables[-1]
334290

335291
args, _, blocks, ret_type, fn_info = builder.leave()
336292

337-
if fn_info.is_generator:
338-
add_methods_to_generator_class(builder, fn_info, sig, args, blocks, fitem.is_coroutine)
339-
else:
340-
func_ir, func_reg = gen_func_ir(
341-
builder, args, blocks, sig, fn_info, cdef, is_singledispatch
342-
)
293+
func_ir, func_reg = gen_func_ir(builder, args, blocks, sig, fn_info, cdef, is_singledispatch)
343294

344295
# Evaluate argument defaults in the surrounding scope, since we
345296
# calculate them *once* when the function definition is evaluated.
346297
calculate_arg_defaults(builder, fn_info, func_reg, symtable)
347-
348-
if is_singledispatch:
349-
# add the generated main singledispatch function
350-
builder.functions.append(func_ir)
351-
# create the dispatch function
352-
assert isinstance(fitem, FuncDef)
353-
return gen_dispatch_func_ir(builder, fitem, fn_info.name, name, sig)
354-
355298
return func_ir, func_reg
356299

357300

@@ -512,33 +455,6 @@ def handle_non_ext_method(
512455
builder.add_to_non_ext_dict(non_ext, name, func_reg, fdef.line)
513456

514457

515-
def calculate_arg_defaults(
516-
builder: IRBuilder,
517-
fn_info: FuncInfo,
518-
func_reg: Value | None,
519-
symtable: dict[SymbolNode, SymbolTarget],
520-
) -> None:
521-
"""Calculate default argument values and store them.
522-
523-
They are stored in statics for top level functions and in
524-
the function objects for nested functions (while constants are
525-
still stored computed on demand).
526-
"""
527-
fitem = fn_info.fitem
528-
for arg in fitem.arguments:
529-
# Constant values don't get stored but just recomputed
530-
if arg.initializer and not is_constant(arg.initializer):
531-
value = builder.coerce(
532-
builder.accept(arg.initializer), symtable[arg.variable].type, arg.line
533-
)
534-
if not fn_info.is_nested:
535-
name = fitem.fullname + "." + arg.variable.name
536-
builder.add(InitStatic(value, name, builder.module_name))
537-
else:
538-
assert func_reg is not None
539-
builder.add(SetAttr(func_reg, arg.variable.name, value, arg.line))
540-
541-
542458
def gen_func_ns(builder: IRBuilder) -> str:
543459
"""Generate a namespace for a nested function using its outer function names."""
544460
return "_".join(

0 commit comments

Comments
 (0)