Skip to content

Commit f2ead85

Browse files
authored
[mypyc] Refactor: extract code from mypyc.irbuild.function (#8508)
This extracts these things to new modules: * Code related to generator functions (not all of it, since some was hard to extract) * Code related to callable classes * Code related to environment classes Work on mypyc/mypyc#714.
1 parent 492876f commit f2ead85

File tree

5 files changed

+737
-664
lines changed

5 files changed

+737
-664
lines changed

mypyc/irbuild/builder.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
from mypyc.irbuild.context import FuncInfo, ImplicitClass
6565
from mypyc.irbuild.mapper import Mapper
6666
from mypyc.irbuild.ll_builder import LowLevelIRBuilder
67+
from mypyc.irbuild.util import is_constant
6768

6869
GenFunc = Callable[[], None]
6970

@@ -1187,3 +1188,36 @@ def warning(self, msg: str, line: int) -> None:
11871188

11881189
def error(self, msg: str, line: int) -> None:
11891190
self.errors.error(msg, self.module_path, line)
1191+
1192+
1193+
def gen_arg_defaults(builder: IRBuilder) -> None:
1194+
"""Generate blocks for arguments that have default values.
1195+
1196+
If the passed value is an error value, then assign the default
1197+
value to the argument.
1198+
"""
1199+
fitem = builder.fn_info.fitem
1200+
for arg in fitem.arguments:
1201+
if arg.initializer:
1202+
target = builder.environment.lookup(arg.variable)
1203+
1204+
def get_default() -> Value:
1205+
assert arg.initializer is not None
1206+
1207+
# If it is constant, don't bother storing it
1208+
if is_constant(arg.initializer):
1209+
return builder.accept(arg.initializer)
1210+
1211+
# Because gen_arg_defaults runs before calculate_arg_defaults, we
1212+
# add the static/attribute to final_names/the class here.
1213+
elif not builder.fn_info.is_nested:
1214+
name = fitem.fullname + '.' + arg.variable.name
1215+
builder.final_names.append((name, target.type))
1216+
return builder.add(LoadStatic(target.type, name, builder.module_name))
1217+
else:
1218+
name = arg.variable.name
1219+
builder.fn_info.callable_class.ir.attributes[name] = target.type
1220+
return builder.add(
1221+
GetAttr(builder.fn_info.callable_class.self_reg, name, arg.line))
1222+
assert isinstance(target, AssignmentTargetRegister)
1223+
builder.assign_if_null(target, get_default, arg.initializer.line)

mypyc/irbuild/callable_class.py

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
"""Generate a class that represents a nested function.
2+
3+
The class defines __call__ for calling the function and allows access to variables
4+
defined in outer scopes.
5+
"""
6+
7+
from typing import List
8+
9+
from mypy.nodes import Var
10+
11+
from mypyc.common import SELF_NAME, ENV_ATTR_NAME
12+
from mypyc.ir.ops import BasicBlock, Return, Call, SetAttr, Value, Environment
13+
from mypyc.ir.rtypes import RInstance, object_rprimitive
14+
from mypyc.ir.func_ir import FuncIR, FuncSignature, RuntimeArg, FuncDecl
15+
from mypyc.ir.class_ir import ClassIR
16+
from mypyc.irbuild.builder import IRBuilder
17+
from mypyc.irbuild.context import FuncInfo, ImplicitClass
18+
from mypyc.irbuild.util import add_self_to_env
19+
from mypyc.primitives.misc_ops import method_new_op
20+
21+
22+
def setup_callable_class(builder: IRBuilder) -> None:
23+
"""Generates a callable class representing a nested function or a function within a
24+
non-extension class and sets up the 'self' variable for that class.
25+
26+
This takes the most recently visited function and returns a ClassIR to represent that
27+
function. Each callable class contains an environment attribute with points to another
28+
ClassIR representing the environment class where some of its variables can be accessed.
29+
Note that its '__call__' method is not yet implemented, and is implemented in the
30+
add_call_to_callable_class function.
31+
32+
Returns a newly constructed ClassIR representing the callable class for the nested
33+
function.
34+
"""
35+
36+
# Check to see that the name has not already been taken. If so, rename the class. We allow
37+
# multiple uses of the same function name because this is valid in if-else blocks. Example:
38+
# if True:
39+
# def foo(): ----> foo_obj()
40+
# return True
41+
# else:
42+
# def foo(): ----> foo_obj_0()
43+
# return False
44+
name = base_name = '{}_obj'.format(builder.fn_info.namespaced_name())
45+
count = 0
46+
while name in builder.callable_class_names:
47+
name = base_name + '_' + str(count)
48+
count += 1
49+
builder.callable_class_names.add(name)
50+
51+
# Define the actual callable class ClassIR, and set its environment to point at the
52+
# previously defined environment class.
53+
callable_class_ir = ClassIR(name, builder.module_name, is_generated=True)
54+
55+
# The functools @wraps decorator attempts to call setattr on nested functions, so
56+
# we create a dict for these nested functions.
57+
# https://github.com/python/cpython/blob/3.7/Lib/functools.py#L58
58+
if builder.fn_info.is_nested:
59+
callable_class_ir.has_dict = True
60+
61+
# If the enclosing class doesn't contain nested (which will happen if
62+
# this is a toplevel lambda), don't set up an environment.
63+
if builder.fn_infos[-2].contains_nested:
64+
callable_class_ir.attributes[ENV_ATTR_NAME] = RInstance(
65+
builder.fn_infos[-2].env_class
66+
)
67+
callable_class_ir.mro = [callable_class_ir]
68+
builder.fn_info.callable_class = ImplicitClass(callable_class_ir)
69+
builder.classes.append(callable_class_ir)
70+
71+
# Add a 'self' variable to the callable class' environment, and store that variable in a
72+
# register to be accessed later.
73+
self_target = add_self_to_env(builder.environment, callable_class_ir)
74+
builder.fn_info.callable_class.self_reg = builder.read(self_target, builder.fn_info.fitem.line)
75+
76+
77+
def add_call_to_callable_class(builder: IRBuilder,
78+
blocks: List[BasicBlock],
79+
sig: FuncSignature,
80+
env: Environment,
81+
fn_info: FuncInfo) -> FuncIR:
82+
"""Generates a '__call__' method for a callable class representing a nested function.
83+
84+
This takes the blocks, signature, and environment associated with a function definition and
85+
uses those to build the '__call__' method of a given callable class, used to represent that
86+
function. Note that a 'self' parameter is added to its list of arguments, as the nested
87+
function becomes a class method.
88+
"""
89+
sig = FuncSignature((RuntimeArg(SELF_NAME, object_rprimitive),) + sig.args, sig.ret_type)
90+
call_fn_decl = FuncDecl('__call__', fn_info.callable_class.ir.name, builder.module_name, sig)
91+
call_fn_ir = FuncIR(call_fn_decl, blocks, env,
92+
fn_info.fitem.line, traceback_name=fn_info.fitem.name)
93+
fn_info.callable_class.ir.methods['__call__'] = call_fn_ir
94+
return call_fn_ir
95+
96+
97+
def add_get_to_callable_class(builder: IRBuilder, fn_info: FuncInfo) -> None:
98+
"""Generates the '__get__' method for a callable class."""
99+
line = fn_info.fitem.line
100+
builder.enter(fn_info)
101+
102+
vself = builder.read(
103+
builder.environment.add_local_reg(Var(SELF_NAME), object_rprimitive, True)
104+
)
105+
instance = builder.environment.add_local_reg(Var('instance'), object_rprimitive, True)
106+
builder.environment.add_local_reg(Var('owner'), object_rprimitive, True)
107+
108+
# If accessed through the class, just return the callable
109+
# object. If accessed through an object, create a new bound
110+
# instance method object.
111+
instance_block, class_block = BasicBlock(), BasicBlock()
112+
comparison = builder.binary_op(
113+
builder.read(instance), builder.none_object(), 'is', line
114+
)
115+
builder.add_bool_branch(comparison, class_block, instance_block)
116+
117+
builder.activate_block(class_block)
118+
builder.add(Return(vself))
119+
120+
builder.activate_block(instance_block)
121+
builder.add(Return(builder.primitive_op(method_new_op, [vself, builder.read(instance)], line)))
122+
123+
blocks, env, _, fn_info = builder.leave()
124+
125+
sig = FuncSignature((RuntimeArg(SELF_NAME, object_rprimitive),
126+
RuntimeArg('instance', object_rprimitive),
127+
RuntimeArg('owner', object_rprimitive)),
128+
object_rprimitive)
129+
get_fn_decl = FuncDecl('__get__', fn_info.callable_class.ir.name, builder.module_name, sig)
130+
get_fn_ir = FuncIR(get_fn_decl, blocks, env)
131+
fn_info.callable_class.ir.methods['__get__'] = get_fn_ir
132+
builder.functions.append(get_fn_ir)
133+
134+
135+
def instantiate_callable_class(builder: IRBuilder, fn_info: FuncInfo) -> Value:
136+
"""
137+
Assigns a callable class to a register named after the given function definition. Note
138+
that fn_info refers to the function being assigned, whereas builder.fn_info refers to the
139+
function encapsulating the function being turned into a callable class.
140+
"""
141+
fitem = fn_info.fitem
142+
func_reg = builder.add(Call(fn_info.callable_class.ir.ctor, [], fitem.line))
143+
144+
# Set the callable class' environment attribute to point at the environment class
145+
# defined in the callable class' immediate outer scope. Note that there are three possible
146+
# environment class registers we may use. If the encapsulating function is:
147+
# - a generator function, then the callable class is instantiated from the generator class'
148+
# __next__' function, and hence the generator class' environment register is used.
149+
# - a nested function, then the callable class is instantiated from the current callable
150+
# class' '__call__' function, and hence the callable class' environment register is used.
151+
# - neither, then we use the environment register of the original function.
152+
curr_env_reg = None
153+
if builder.fn_info.is_generator:
154+
curr_env_reg = builder.fn_info.generator_class.curr_env_reg
155+
elif builder.fn_info.is_nested:
156+
curr_env_reg = builder.fn_info.callable_class.curr_env_reg
157+
elif builder.fn_info.contains_nested:
158+
curr_env_reg = builder.fn_info.curr_env_reg
159+
if curr_env_reg:
160+
builder.add(SetAttr(func_reg, ENV_ATTR_NAME, curr_env_reg, fitem.line))
161+
return func_reg

mypyc/irbuild/env_class.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
"""Generate classes representing function environments (+ related operations)."""
2+
3+
from typing import Optional, Union
4+
5+
from mypy.nodes import FuncDef, SymbolNode
6+
7+
from mypyc.common import SELF_NAME, ENV_ATTR_NAME
8+
from mypyc.ir.ops import Call, GetAttr, SetAttr, Value, Environment, AssignmentTargetAttr
9+
from mypyc.ir.rtypes import RInstance, object_rprimitive
10+
from mypyc.ir.class_ir import ClassIR
11+
from mypyc.irbuild.builder import IRBuilder
12+
from mypyc.irbuild.context import FuncInfo, ImplicitClass, GeneratorClass
13+
14+
15+
def setup_env_class(builder: IRBuilder) -> ClassIR:
16+
"""Generates a class representing a function environment.
17+
18+
Note that the variables in the function environment are not actually populated here. This
19+
is because when the environment class is generated, the function environment has not yet
20+
been visited. This behavior is allowed so that when the compiler visits nested functions,
21+
it can use the returned ClassIR instance to figure out free variables it needs to access.
22+
The remaining attributes of the environment class are populated when the environment
23+
registers are loaded.
24+
25+
Returns a ClassIR representing an environment for a function containing a nested function.
26+
"""
27+
env_class = ClassIR('{}_env'.format(builder.fn_info.namespaced_name()),
28+
builder.module_name, is_generated=True)
29+
env_class.attributes[SELF_NAME] = RInstance(env_class)
30+
if builder.fn_info.is_nested:
31+
# If the function is nested, its environment class must contain an environment
32+
# attribute pointing to its encapsulating functions' environment class.
33+
env_class.attributes[ENV_ATTR_NAME] = RInstance(builder.fn_infos[-2].env_class)
34+
env_class.mro = [env_class]
35+
builder.fn_info.env_class = env_class
36+
builder.classes.append(env_class)
37+
return env_class
38+
39+
40+
def finalize_env_class(builder: IRBuilder) -> None:
41+
"""Generates, instantiates, and sets up the environment of an environment class."""
42+
43+
instantiate_env_class(builder)
44+
45+
# Iterate through the function arguments and replace local definitions (using registers)
46+
# that were previously added to the environment with references to the function's
47+
# environment class.
48+
if builder.fn_info.is_nested:
49+
add_args_to_env(builder, local=False, base=builder.fn_info.callable_class)
50+
else:
51+
add_args_to_env(builder, local=False, base=builder.fn_info)
52+
53+
54+
def instantiate_env_class(builder: IRBuilder) -> Value:
55+
"""Assigns an environment class to a register named after the given function definition."""
56+
curr_env_reg = builder.add(
57+
Call(builder.fn_info.env_class.ctor, [], builder.fn_info.fitem.line)
58+
)
59+
60+
if builder.fn_info.is_nested:
61+
builder.fn_info.callable_class._curr_env_reg = curr_env_reg
62+
builder.add(SetAttr(curr_env_reg,
63+
ENV_ATTR_NAME,
64+
builder.fn_info.callable_class.prev_env_reg,
65+
builder.fn_info.fitem.line))
66+
else:
67+
builder.fn_info._curr_env_reg = curr_env_reg
68+
69+
return curr_env_reg
70+
71+
72+
def load_env_registers(builder: IRBuilder) -> None:
73+
"""Loads the registers for the current FuncItem being visited.
74+
75+
Adds the arguments of the FuncItem to the environment. If the FuncItem is nested inside of
76+
another function, then this also loads all of the outer environments of the FuncItem into
77+
registers so that they can be used when accessing free variables.
78+
"""
79+
add_args_to_env(builder, local=True)
80+
81+
fn_info = builder.fn_info
82+
fitem = fn_info.fitem
83+
if fn_info.is_nested:
84+
load_outer_envs(builder, fn_info.callable_class)
85+
# If this is a FuncDef, then make sure to load the FuncDef into its own environment
86+
# class so that the function can be called recursively.
87+
if isinstance(fitem, FuncDef):
88+
setup_func_for_recursive_call(builder, fitem, fn_info.callable_class)
89+
90+
91+
def load_outer_env(builder: IRBuilder, base: Value, outer_env: Environment) -> Value:
92+
"""Loads the environment class for a given base into a register.
93+
94+
Additionally, iterates through all of the SymbolNode and AssignmentTarget instances of the
95+
environment at the given index's symtable, and adds those instances to the environment of
96+
the current environment. This is done so that the current environment can access outer
97+
environment variables without having to reload all of the environment registers.
98+
99+
Returns the register where the environment class was loaded.
100+
"""
101+
env = builder.add(GetAttr(base, ENV_ATTR_NAME, builder.fn_info.fitem.line))
102+
assert isinstance(env.type, RInstance), '{} must be of type RInstance'.format(env)
103+
104+
for symbol, target in outer_env.symtable.items():
105+
env.type.class_ir.attributes[symbol.name] = target.type
106+
symbol_target = AssignmentTargetAttr(env, symbol.name)
107+
builder.environment.add_target(symbol, symbol_target)
108+
109+
return env
110+
111+
112+
def load_outer_envs(builder: IRBuilder, base: ImplicitClass) -> None:
113+
index = len(builder.builders) - 2
114+
115+
# Load the first outer environment. This one is special because it gets saved in the
116+
# FuncInfo instance's prev_env_reg field.
117+
if index > 1:
118+
# outer_env = builder.fn_infos[index].environment
119+
outer_env = builder.builders[index].environment
120+
if isinstance(base, GeneratorClass):
121+
base.prev_env_reg = load_outer_env(builder, base.curr_env_reg, outer_env)
122+
else:
123+
base.prev_env_reg = load_outer_env(builder, base.self_reg, outer_env)
124+
env_reg = base.prev_env_reg
125+
index -= 1
126+
127+
# Load the remaining outer environments into registers.
128+
while index > 1:
129+
# outer_env = builder.fn_infos[index].environment
130+
outer_env = builder.builders[index].environment
131+
env_reg = load_outer_env(builder, env_reg, outer_env)
132+
index -= 1
133+
134+
135+
def add_args_to_env(builder: IRBuilder,
136+
local: bool = True,
137+
base: Optional[Union[FuncInfo, ImplicitClass]] = None,
138+
reassign: bool = True) -> None:
139+
fn_info = builder.fn_info
140+
if local:
141+
for arg in fn_info.fitem.arguments:
142+
rtype = builder.type_to_rtype(arg.variable.type)
143+
builder.environment.add_local_reg(arg.variable, rtype, is_arg=True)
144+
else:
145+
for arg in fn_info.fitem.arguments:
146+
if is_free_variable(builder, arg.variable) or fn_info.is_generator:
147+
rtype = builder.type_to_rtype(arg.variable.type)
148+
assert base is not None, 'base cannot be None for adding nonlocal args'
149+
builder.add_var_to_env_class(arg.variable, rtype, base, reassign=reassign)
150+
151+
152+
def setup_func_for_recursive_call(builder: IRBuilder, fdef: FuncDef, base: ImplicitClass) -> None:
153+
"""
154+
Adds the instance of the callable class representing the given FuncDef to a register in the
155+
environment so that the function can be called recursively. Note that this needs to be done
156+
only for nested functions.
157+
"""
158+
# First, set the attribute of the environment class so that GetAttr can be called on it.
159+
prev_env = builder.fn_infos[-2].env_class
160+
prev_env.attributes[fdef.name] = builder.type_to_rtype(fdef.type)
161+
162+
if isinstance(base, GeneratorClass):
163+
# If we are dealing with a generator class, then we need to first get the register
164+
# holding the current environment class, and load the previous environment class from
165+
# there.
166+
prev_env_reg = builder.add(GetAttr(base.curr_env_reg, ENV_ATTR_NAME, -1))
167+
else:
168+
prev_env_reg = base.prev_env_reg
169+
170+
# Obtain the instance of the callable class representing the FuncDef, and add it to the
171+
# current environment.
172+
val = builder.add(GetAttr(prev_env_reg, fdef.name, -1))
173+
target = builder.environment.add_local_reg(fdef, object_rprimitive)
174+
builder.assign(target, val, -1)
175+
176+
177+
def is_free_variable(builder: IRBuilder, symbol: SymbolNode) -> bool:
178+
fitem = builder.fn_info.fitem
179+
return (
180+
fitem in builder.free_variables
181+
and symbol in builder.free_variables[fitem]
182+
)

0 commit comments

Comments
 (0)