This commit is contained in:
Waylon Walker 2022-03-31 20:20:07 -05:00
commit 38355d2442
No known key found for this signature in database
GPG key ID: 66E2BF2B4190EFE4
9083 changed files with 1225834 additions and 0 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,164 @@
"""Generate a class that represents a nested function.
The class defines __call__ for calling the function and allows access to
non-local variables defined in outer scopes.
"""
from typing import List
from mypyc.common import SELF_NAME, ENV_ATTR_NAME
from mypyc.ir.ops import BasicBlock, Return, Call, SetAttr, Value, Register
from mypyc.ir.rtypes import RInstance, object_rprimitive
from mypyc.ir.func_ir import FuncIR, FuncSignature, RuntimeArg, FuncDecl
from mypyc.ir.class_ir import ClassIR
from mypyc.irbuild.builder import IRBuilder
from mypyc.irbuild.context import FuncInfo, ImplicitClass
from mypyc.primitives.misc_ops import method_new_op
def setup_callable_class(builder: IRBuilder) -> None:
"""Generate an (incomplete) callable class representing function.
This can be a nested function or a function within a non-extension
class. Also set up the 'self' variable for that class.
This takes the most recently visited function and returns a
ClassIR to represent that function. Each callable class contains
an environment attribute which points to another ClassIR
representing the environment class where some of its variables can
be accessed.
Note that some methods, such as '__call__', are not yet
created here. Use additional functions, such as
add_call_to_callable_class(), to add them.
Return a newly constructed ClassIR representing the callable
class for the nested function.
"""
# Check to see that the name has not already been taken. If so,
# rename the class. We allow multiple uses of the same function
# name because this is valid in if-else blocks. Example:
#
# if True:
# def foo(): ----> foo_obj()
# return True
# else:
# def foo(): ----> foo_obj_0()
# return False
name = base_name = '{}_obj'.format(builder.fn_info.namespaced_name())
count = 0
while name in builder.callable_class_names:
name = base_name + '_' + str(count)
count += 1
builder.callable_class_names.add(name)
# Define the actual callable class ClassIR, and set its
# environment to point at the previously defined environment
# class.
callable_class_ir = ClassIR(name, builder.module_name, is_generated=True)
# The functools @wraps decorator attempts to call setattr on
# nested functions, so we create a dict for these nested
# functions.
# https://github.com/python/cpython/blob/3.7/Lib/functools.py#L58
if builder.fn_info.is_nested:
callable_class_ir.has_dict = True
# If the enclosing class doesn't contain nested (which will happen if
# this is a toplevel lambda), don't set up an environment.
if builder.fn_infos[-2].contains_nested:
callable_class_ir.attributes[ENV_ATTR_NAME] = RInstance(
builder.fn_infos[-2].env_class
)
callable_class_ir.mro = [callable_class_ir]
builder.fn_info.callable_class = ImplicitClass(callable_class_ir)
builder.classes.append(callable_class_ir)
# Add a 'self' variable to the environment of the callable class,
# and store that variable in a register to be accessed later.
self_target = builder.add_self_to_env(callable_class_ir)
builder.fn_info.callable_class.self_reg = builder.read(self_target, builder.fn_info.fitem.line)
def add_call_to_callable_class(builder: IRBuilder,
args: List[Register],
blocks: List[BasicBlock],
sig: FuncSignature,
fn_info: FuncInfo) -> FuncIR:
"""Generate a '__call__' method for a callable class representing a nested function.
This takes the blocks and signature associated with a function
definition and uses those to build the '__call__' method of a
given callable class, used to represent that function.
"""
# Since we create a method, we also add a 'self' parameter.
sig = FuncSignature((RuntimeArg(SELF_NAME, object_rprimitive),) + sig.args, sig.ret_type)
call_fn_decl = FuncDecl('__call__', fn_info.callable_class.ir.name, builder.module_name, sig)
call_fn_ir = FuncIR(call_fn_decl, args, blocks,
fn_info.fitem.line, traceback_name=fn_info.fitem.name)
fn_info.callable_class.ir.methods['__call__'] = call_fn_ir
fn_info.callable_class.ir.method_decls['__call__'] = call_fn_decl
return call_fn_ir
def add_get_to_callable_class(builder: IRBuilder, fn_info: FuncInfo) -> None:
"""Generate the '__get__' method for a callable class."""
line = fn_info.fitem.line
with builder.enter_method(
fn_info.callable_class.ir, '__get__', object_rprimitive, fn_info,
self_type=object_rprimitive):
instance = builder.add_argument('instance', object_rprimitive)
builder.add_argument('owner', object_rprimitive)
# If accessed through the class, just return the callable
# object. If accessed through an object, create a new bound
# instance method object.
instance_block, class_block = BasicBlock(), BasicBlock()
comparison = builder.translate_is_op(
builder.read(instance), builder.none_object(), 'is', line
)
builder.add_bool_branch(comparison, class_block, instance_block)
builder.activate_block(class_block)
builder.add(Return(builder.self()))
builder.activate_block(instance_block)
builder.add(Return(builder.call_c(method_new_op,
[builder.self(), builder.read(instance)], line)))
def instantiate_callable_class(builder: IRBuilder, fn_info: FuncInfo) -> Value:
"""Create an instance of a callable class for a function.
Calls to the function will actually call this instance.
Note that fn_info refers to the function being assigned, whereas
builder.fn_info refers to the function encapsulating the function
being turned into a callable class.
"""
fitem = fn_info.fitem
func_reg = builder.add(Call(fn_info.callable_class.ir.ctor, [], fitem.line))
# Set the environment attribute of the callable class to point at
# the environment class defined in the callable class' immediate
# outer scope. Note that there are three possible environment
# class registers we may use. This depends on what the encapsulating
# (parent) function is:
#
# - A nested function: the callable class is instantiated
# from the current callable class' '__call__' function, and hence
# the callable class' environment register is used.
# - A generator function: the callable class is instantiated
# from the '__next__' method of the generator class, and hence the
# environment of the generator class is used.
# - Regular function: we use the environment of the original function.
curr_env_reg = None
if builder.fn_info.is_generator:
curr_env_reg = builder.fn_info.generator_class.curr_env_reg
elif builder.fn_info.is_nested:
curr_env_reg = builder.fn_info.callable_class.curr_env_reg
elif builder.fn_info.contains_nested:
curr_env_reg = builder.fn_info.curr_env_reg
if curr_env_reg:
builder.add(SetAttr(func_reg, ENV_ATTR_NAME, curr_env_reg, fitem.line))
return func_reg

View file

@ -0,0 +1,702 @@
"""Transform class definitions from the mypy AST form to IR."""
from abc import abstractmethod
from typing import Callable, List, Optional, Tuple
from typing_extensions import Final
from mypy.nodes import (
ClassDef, FuncDef, OverloadedFuncDef, PassStmt, AssignmentStmt, CallExpr, NameExpr, StrExpr,
ExpressionStmt, TempNode, Decorator, Lvalue, MemberExpr, RefExpr, TypeInfo, is_class_var
)
from mypy.types import Instance, get_proper_type, ENUM_REMOVED_PROPS
from mypyc.ir.ops import (
Value, Register, Call, LoadErrorValue, LoadStatic, InitStatic, TupleSet, SetAttr, Return,
BasicBlock, Branch, MethodCall, NAMESPACE_TYPE, LoadAddress
)
from mypyc.ir.rtypes import (
RType, object_rprimitive, bool_rprimitive, dict_rprimitive, is_optional_type,
is_object_rprimitive, is_none_rprimitive
)
from mypyc.ir.func_ir import FuncDecl, FuncSignature
from mypyc.ir.class_ir import ClassIR, NonExtClassInfo
from mypyc.primitives.generic_ops import py_setattr_op, py_hasattr_op
from mypyc.primitives.misc_ops import (
dataclass_sleight_of_hand, pytype_from_template_op, py_calc_meta_op, type_object_op,
not_implemented_op
)
from mypyc.primitives.dict_ops import dict_set_item_op, dict_new_op
from mypyc.irbuild.util import (
is_dataclass_decorator, get_func_def, is_constant, dataclass_type
)
from mypyc.irbuild.builder import IRBuilder
from mypyc.irbuild.function import handle_ext_method, handle_non_ext_method, load_type
def transform_class_def(builder: IRBuilder, cdef: ClassDef) -> None:
"""Create IR for a class definition.
This can generate both extension (native) and non-extension
classes. These are generated in very different ways. In the
latter case we construct a Python type object at runtime by doing
the equivalent of "type(name, bases, dict)" in IR. Extension
classes are defined via C structs that are generated later in
mypyc.codegen.emitclass.
This is the main entry point to this module.
"""
ir = builder.mapper.type_to_ir[cdef.info]
# We do this check here because the base field of parent
# classes aren't necessarily populated yet at
# prepare_class_def time.
if any(ir.base_mro[i].base != ir. base_mro[i + 1] for i in range(len(ir.base_mro) - 1)):
builder.error("Non-trait MRO must be linear", cdef.line)
if ir.allow_interpreted_subclasses:
for parent in ir.mro:
if not parent.allow_interpreted_subclasses:
builder.error(
'Base class "{}" does not allow interpreted subclasses'.format(
parent.fullname), cdef.line)
# Currently, we only create non-extension classes for classes that are
# decorated or inherit from Enum. Classes decorated with @trait do not
# apply here, and are handled in a different way.
if ir.is_ext_class:
cls_type = dataclass_type(cdef)
if cls_type is None:
cls_builder: ClassBuilder = ExtClassBuilder(builder, cdef)
elif cls_type in ['dataclasses', 'attr-auto']:
cls_builder = DataClassBuilder(builder, cdef)
elif cls_type == 'attr':
cls_builder = AttrsClassBuilder(builder, cdef)
else:
raise ValueError(cls_type)
else:
cls_builder = NonExtClassBuilder(builder, cdef)
for stmt in cdef.defs.body:
if isinstance(stmt, OverloadedFuncDef) and stmt.is_property:
if isinstance(cls_builder, NonExtClassBuilder):
# properties with both getters and setters in non_extension
# classes not supported
builder.error("Property setters not supported in non-extension classes",
stmt.line)
for item in stmt.items:
with builder.catch_errors(stmt.line):
cls_builder.add_method(get_func_def(item))
elif isinstance(stmt, (FuncDef, Decorator, OverloadedFuncDef)):
# Ignore plugin generated methods (since they have no
# bodies to compile and will need to have the bodies
# provided by some other mechanism.)
if cdef.info.names[stmt.name].plugin_generated:
continue
with builder.catch_errors(stmt.line):
cls_builder.add_method(get_func_def(stmt))
elif isinstance(stmt, PassStmt):
continue
elif isinstance(stmt, AssignmentStmt):
if len(stmt.lvalues) != 1:
builder.error("Multiple assignment in class bodies not supported", stmt.line)
continue
lvalue = stmt.lvalues[0]
if not isinstance(lvalue, NameExpr):
builder.error("Only assignment to variables is supported in class bodies",
stmt.line)
continue
# We want to collect class variables in a dictionary for both real
# non-extension classes and fake dataclass ones.
cls_builder.add_attr(lvalue, stmt)
elif isinstance(stmt, ExpressionStmt) and isinstance(stmt.expr, StrExpr):
# Docstring. Ignore
pass
else:
builder.error("Unsupported statement in class body", stmt.line)
cls_builder.finalize(ir)
class ClassBuilder:
"""Create IR for a class definition.
This is an abstract base class.
"""
def __init__(self, builder: IRBuilder, cdef: ClassDef) -> None:
self.builder = builder
self.cdef = cdef
self.attrs_to_cache: List[Tuple[Lvalue, RType]] = []
@abstractmethod
def add_method(self, fdef: FuncDef) -> None:
"""Add a method to the class IR"""
@abstractmethod
def add_attr(self, lvalue: NameExpr, stmt: AssignmentStmt) -> None:
"""Add an attribute to the class IR"""
@abstractmethod
def finalize(self, ir: ClassIR) -> None:
"""Perform any final operations to complete the class IR"""
class NonExtClassBuilder(ClassBuilder):
def __init__(self, builder: IRBuilder, cdef: ClassDef) -> None:
super().__init__(builder, cdef)
self.non_ext = self.create_non_ext_info()
def create_non_ext_info(self) -> NonExtClassInfo:
non_ext_bases = populate_non_ext_bases(self.builder, self.cdef)
non_ext_metaclass = find_non_ext_metaclass(self.builder, self.cdef, non_ext_bases)
non_ext_dict = setup_non_ext_dict(self.builder, self.cdef, non_ext_metaclass,
non_ext_bases)
# We populate __annotations__ for non-extension classes
# because dataclasses uses it to determine which attributes to compute on.
# TODO: Maybe generate more precise types for annotations
non_ext_anns = self.builder.call_c(dict_new_op, [], self.cdef.line)
return NonExtClassInfo(non_ext_dict, non_ext_bases, non_ext_anns, non_ext_metaclass)
def add_method(self, fdef: FuncDef) -> None:
handle_non_ext_method(self.builder, self.non_ext, self.cdef, fdef)
def add_attr(self, lvalue: NameExpr, stmt: AssignmentStmt) -> None:
add_non_ext_class_attr_ann(self.builder, self.non_ext, lvalue, stmt)
add_non_ext_class_attr(self.builder, self.non_ext, lvalue, stmt, self.cdef,
self.attrs_to_cache)
def finalize(self, ir: ClassIR) -> None:
# Dynamically create the class via the type constructor
non_ext_class = load_non_ext_class(self.builder, ir, self.non_ext, self.cdef.line)
non_ext_class = load_decorated_class(self.builder, self.cdef, non_ext_class)
# Save the decorated class
self.builder.add(InitStatic(non_ext_class, self.cdef.name, self.builder.module_name,
NAMESPACE_TYPE))
# Add the non-extension class to the dict
self.builder.call_c(dict_set_item_op,
[
self.builder.load_globals_dict(),
self.builder.load_str(self.cdef.name),
non_ext_class
], self.cdef.line)
# Cache any cacheable class attributes
cache_class_attrs(self.builder, self.attrs_to_cache, self.cdef)
class ExtClassBuilder(ClassBuilder):
def __init__(self, builder: IRBuilder, cdef: ClassDef) -> None:
super().__init__(builder, cdef)
# If the class is not decorated, generate an extension class for it.
self.type_obj: Optional[Value] = allocate_class(builder, cdef)
def skip_attr_default(self, name: str, stmt: AssignmentStmt) -> bool:
"""Controls whether to skip generating a default for an attribute."""
return False
def add_method(self, fdef: FuncDef) -> None:
handle_ext_method(self.builder, self.cdef, fdef)
def add_attr(self, lvalue: NameExpr, stmt: AssignmentStmt) -> None:
# Variable declaration with no body
if isinstance(stmt.rvalue, TempNode):
return
# Only treat marked class variables as class variables.
if not (is_class_var(lvalue) or stmt.is_final_def):
return
typ = self.builder.load_native_type_object(self.cdef.fullname)
value = self.builder.accept(stmt.rvalue)
self.builder.call_c(
py_setattr_op, [typ, self.builder.load_str(lvalue.name), value], stmt.line)
if self.builder.non_function_scope() and stmt.is_final_def:
self.builder.init_final_static(lvalue, value, self.cdef.name)
def finalize(self, ir: ClassIR) -> None:
generate_attr_defaults(self.builder, self.cdef, self.skip_attr_default)
create_ne_from_eq(self.builder, self.cdef)
class DataClassBuilder(ExtClassBuilder):
# controls whether an __annotations__ attribute should be added to the class
# __dict__. This is not desirable for attrs classes where auto_attribs is
# disabled, as attrs will reject it.
add_annotations_to_dict = True
def __init__(self, builder: IRBuilder, cdef: ClassDef) -> None:
super().__init__(builder, cdef)
self.non_ext = self.create_non_ext_info()
def create_non_ext_info(self) -> NonExtClassInfo:
"""Set up a NonExtClassInfo to track dataclass attributes.
In addition to setting up a normal extension class for dataclasses,
we also collect its class attributes like a non-extension class so
that we can hand them to the dataclass decorator.
"""
return NonExtClassInfo(
self.builder.call_c(dict_new_op, [], self.cdef.line),
self.builder.add(TupleSet([], self.cdef.line)),
self.builder.call_c(dict_new_op, [], self.cdef.line),
self.builder.add(LoadAddress(type_object_op.type, type_object_op.src, self.cdef.line))
)
def skip_attr_default(self, name: str, stmt: AssignmentStmt) -> bool:
return stmt.type is not None
def get_type_annotation(self, stmt: AssignmentStmt) -> Optional[TypeInfo]:
# We populate __annotations__ because dataclasses uses it to determine
# which attributes to compute on.
ann_type = get_proper_type(stmt.type)
if isinstance(ann_type, Instance):
return ann_type.type
return None
def add_attr(self, lvalue: NameExpr, stmt: AssignmentStmt) -> None:
add_non_ext_class_attr_ann(self.builder, self.non_ext, lvalue, stmt,
self.get_type_annotation)
add_non_ext_class_attr(self.builder, self.non_ext, lvalue, stmt, self.cdef,
self.attrs_to_cache)
super().add_attr(lvalue, stmt)
def finalize(self, ir: ClassIR) -> None:
"""Generate code to finish instantiating a dataclass.
This works by replacing all of the attributes on the class
(which will be descriptors) with whatever they would be in a
non-extension class, calling dataclass, then switching them back.
The resulting class is an extension class and instances of it do not
have a __dict__ (unless something else requires it).
All methods written explicitly in the source are compiled and
may be called through the vtable while the methods generated
by dataclasses are interpreted and may not be.
(If we just called dataclass without doing this, it would think that all
of the descriptors for our attributes are default values and generate an
incorrect constructor. We need to do the switch so that dataclass gets the
appropriate defaults.)
"""
super().finalize(ir)
assert self.type_obj
add_dunders_to_non_ext_dict(self.builder, self.non_ext, self.cdef.line,
self.add_annotations_to_dict)
dec = self.builder.accept(
next(d for d in self.cdef.decorators if is_dataclass_decorator(d)))
self.builder.call_c(
dataclass_sleight_of_hand, [dec, self.type_obj, self.non_ext.dict, self.non_ext.anns],
self.cdef.line)
class AttrsClassBuilder(DataClassBuilder):
"""Create IR for an attrs class where auto_attribs=False (the default).
When auto_attribs is enabled, attrs classes behave similarly to dataclasses
(i.e. types are stored as annotations on the class) and are thus handled
by DataClassBuilder, but when auto_attribs is disabled the types are
provided via attr.ib(type=...)
"""
add_annotations_to_dict = False
def skip_attr_default(self, name: str, stmt: AssignmentStmt) -> bool:
return True
def get_type_annotation(self, stmt: AssignmentStmt) -> Optional[TypeInfo]:
if isinstance(stmt.rvalue, CallExpr):
# find the type arg in `attr.ib(type=str)`
callee = stmt.rvalue.callee
if (isinstance(callee, MemberExpr) and
callee.fullname in ['attr.ib', 'attr.attr'] and
'type' in stmt.rvalue.arg_names):
index = stmt.rvalue.arg_names.index('type')
type_name = stmt.rvalue.args[index]
if isinstance(type_name, NameExpr) and isinstance(type_name.node, TypeInfo):
lvalue = stmt.lvalues[0]
assert isinstance(lvalue, NameExpr)
return type_name.node
return None
def allocate_class(builder: IRBuilder, cdef: ClassDef) -> Value:
# OK AND NOW THE FUN PART
base_exprs = cdef.base_type_exprs + cdef.removed_base_type_exprs
if base_exprs:
bases = [builder.accept(x) for x in base_exprs]
tp_bases = builder.new_tuple(bases, cdef.line)
else:
tp_bases = builder.add(LoadErrorValue(object_rprimitive, is_borrowed=True))
modname = builder.load_str(builder.module_name)
template = builder.add(LoadStatic(object_rprimitive, cdef.name + "_template",
builder.module_name, NAMESPACE_TYPE))
# Create the class
tp = builder.call_c(pytype_from_template_op,
[template, tp_bases, modname], cdef.line)
# Immediately fix up the trait vtables, before doing anything with the class.
ir = builder.mapper.type_to_ir[cdef.info]
if not ir.is_trait and not ir.builtin_base:
builder.add(Call(
FuncDecl(cdef.name + '_trait_vtable_setup',
None, builder.module_name,
FuncSignature([], bool_rprimitive)), [], -1))
# Populate a '__mypyc_attrs__' field containing the list of attrs
builder.call_c(py_setattr_op, [
tp, builder.load_str('__mypyc_attrs__'),
create_mypyc_attrs_tuple(builder, builder.mapper.type_to_ir[cdef.info], cdef.line)],
cdef.line)
# Save the class
builder.add(InitStatic(tp, cdef.name, builder.module_name, NAMESPACE_TYPE))
# Add it to the dict
builder.call_c(dict_set_item_op,
[builder.load_globals_dict(),
builder.load_str(cdef.name),
tp],
cdef.line)
return tp
# Mypy uses these internally as base classes of TypedDict classes. These are
# lies and don't have any runtime equivalent.
MAGIC_TYPED_DICT_CLASSES: Final[Tuple[str, ...]] = (
'typing._TypedDict',
'typing_extensions._TypedDict',
)
def populate_non_ext_bases(builder: IRBuilder, cdef: ClassDef) -> Value:
"""Create base class tuple of a non-extension class.
The tuple is passed to the metaclass constructor.
"""
is_named_tuple = cdef.info.is_named_tuple
ir = builder.mapper.type_to_ir[cdef.info]
bases = []
for cls in cdef.info.mro[1:]:
if cls.fullname == 'builtins.object':
continue
if is_named_tuple and cls.fullname in ('typing.Sequence',
'typing.Iterable',
'typing.Collection',
'typing.Reversible',
'typing.Container'):
# HAX: Synthesized base classes added by mypy don't exist at runtime, so skip them.
# This could break if they were added explicitly, though...
continue
# Add the current class to the base classes list of concrete subclasses
if cls in builder.mapper.type_to_ir:
base_ir = builder.mapper.type_to_ir[cls]
if base_ir.children is not None:
base_ir.children.append(ir)
if cls.fullname in MAGIC_TYPED_DICT_CLASSES:
# HAX: Mypy internally represents TypedDict classes differently from what
# should happen at runtime. Replace with something that works.
module = 'typing'
if builder.options.capi_version < (3, 9):
name = 'TypedDict'
if builder.options.capi_version < (3, 8):
# TypedDict was added to typing in Python 3.8.
module = 'typing_extensions'
else:
# In Python 3.9 TypedDict is not a real type.
name = '_TypedDict'
base = builder.get_module_attr(module, name, cdef.line)
elif is_named_tuple and cls.fullname == 'builtins.tuple':
if builder.options.capi_version < (3, 9):
name = 'NamedTuple'
else:
# This was changed in Python 3.9.
name = '_NamedTuple'
base = builder.get_module_attr('typing', name, cdef.line)
else:
base = builder.load_global_str(cls.name, cdef.line)
bases.append(base)
if cls.fullname in MAGIC_TYPED_DICT_CLASSES:
# The remaining base classes are synthesized by mypy and should be ignored.
break
return builder.new_tuple(bases, cdef.line)
def find_non_ext_metaclass(builder: IRBuilder, cdef: ClassDef, bases: Value) -> Value:
"""Find the metaclass of a class from its defs and bases. """
if cdef.metaclass:
declared_metaclass = builder.accept(cdef.metaclass)
else:
if cdef.info.typeddict_type is not None and builder.options.capi_version >= (3, 9):
# In Python 3.9, the metaclass for class-based TypedDict is typing._TypedDictMeta.
# We can't easily calculate it generically, so special case it.
return builder.get_module_attr('typing', '_TypedDictMeta', cdef.line)
elif cdef.info.is_named_tuple and builder.options.capi_version >= (3, 9):
# In Python 3.9, the metaclass for class-based NamedTuple is typing.NamedTupleMeta.
# We can't easily calculate it generically, so special case it.
return builder.get_module_attr('typing', 'NamedTupleMeta', cdef.line)
declared_metaclass = builder.add(LoadAddress(type_object_op.type,
type_object_op.src, cdef.line))
return builder.call_c(py_calc_meta_op, [declared_metaclass, bases], cdef.line)
def setup_non_ext_dict(builder: IRBuilder,
cdef: ClassDef,
metaclass: Value,
bases: Value) -> Value:
"""Initialize the class dictionary for a non-extension class.
This class dictionary is passed to the metaclass constructor.
"""
# Check if the metaclass defines a __prepare__ method, and if so, call it.
has_prepare = builder.call_c(py_hasattr_op,
[metaclass,
builder.load_str('__prepare__')], cdef.line)
non_ext_dict = Register(dict_rprimitive)
true_block, false_block, exit_block, = BasicBlock(), BasicBlock(), BasicBlock()
builder.add_bool_branch(has_prepare, true_block, false_block)
builder.activate_block(true_block)
cls_name = builder.load_str(cdef.name)
prepare_meth = builder.py_get_attr(metaclass, '__prepare__', cdef.line)
prepare_dict = builder.py_call(prepare_meth, [cls_name, bases], cdef.line)
builder.assign(non_ext_dict, prepare_dict, cdef.line)
builder.goto(exit_block)
builder.activate_block(false_block)
builder.assign(non_ext_dict, builder.call_c(dict_new_op, [], cdef.line), cdef.line)
builder.goto(exit_block)
builder.activate_block(exit_block)
return non_ext_dict
def add_non_ext_class_attr_ann(builder: IRBuilder,
non_ext: NonExtClassInfo,
lvalue: NameExpr,
stmt: AssignmentStmt,
get_type_info: Optional[Callable[[AssignmentStmt],
Optional[TypeInfo]]] = None
) -> None:
"""Add a class attribute to __annotations__ of a non-extension class."""
typ: Optional[Value] = None
if get_type_info is not None:
type_info = get_type_info(stmt)
if type_info:
typ = load_type(builder, type_info, stmt.line)
if typ is None:
# FIXME: if get_type_info is not provided, don't fall back to stmt.type?
ann_type = get_proper_type(stmt.type)
if isinstance(ann_type, Instance):
typ = load_type(builder, ann_type.type, stmt.line)
else:
typ = builder.add(LoadAddress(type_object_op.type, type_object_op.src, stmt.line))
key = builder.load_str(lvalue.name)
builder.call_c(dict_set_item_op, [non_ext.anns, key, typ], stmt.line)
def add_non_ext_class_attr(builder: IRBuilder,
non_ext: NonExtClassInfo,
lvalue: NameExpr,
stmt: AssignmentStmt,
cdef: ClassDef,
attr_to_cache: List[Tuple[Lvalue, RType]]) -> None:
"""Add a class attribute to __dict__ of a non-extension class."""
# Only add the attribute to the __dict__ if the assignment is of the form:
# x: type = value (don't add attributes of the form 'x: type' to the __dict__).
if not isinstance(stmt.rvalue, TempNode):
rvalue = builder.accept(stmt.rvalue)
builder.add_to_non_ext_dict(non_ext, lvalue.name, rvalue, stmt.line)
# We cache enum attributes to speed up enum attribute lookup since they
# are final.
if (
cdef.info.bases
and cdef.info.bases[0].type.fullname == 'enum.Enum'
# Skip these since Enum will remove it
and lvalue.name not in ENUM_REMOVED_PROPS
):
# Enum values are always boxed, so use object_rprimitive.
attr_to_cache.append((lvalue, object_rprimitive))
def generate_attr_defaults(builder: IRBuilder, cdef: ClassDef,
skip: Optional[Callable[[str, AssignmentStmt], bool]] = None) -> None:
"""Generate an initialization method for default attr values (from class vars).
If provided, the skip arg should be a callable which will return whether
to skip generating a default for an attribute. It will be passed the name of
the attribute and the corresponding AssignmentStmt.
"""
cls = builder.mapper.type_to_ir[cdef.info]
if cls.builtin_base:
return
# Pull out all assignments in classes in the mro so we can initialize them
# TODO: Support nested statements
default_assignments = []
for info in reversed(cdef.info.mro):
if info not in builder.mapper.type_to_ir:
continue
for stmt in info.defn.defs.body:
if (isinstance(stmt, AssignmentStmt)
and isinstance(stmt.lvalues[0], NameExpr)
and not is_class_var(stmt.lvalues[0])
and not isinstance(stmt.rvalue, TempNode)):
name = stmt.lvalues[0].name
if name == '__slots__':
continue
if name == '__deletable__':
check_deletable_declaration(builder, cls, stmt.line)
continue
if skip is not None and skip(name, stmt):
continue
default_assignments.append(stmt)
if not default_assignments:
return
with builder.enter_method(cls, '__mypyc_defaults_setup', bool_rprimitive):
self_var = builder.self()
for stmt in default_assignments:
lvalue = stmt.lvalues[0]
assert isinstance(lvalue, NameExpr)
if not stmt.is_final_def and not is_constant(stmt.rvalue):
builder.warning('Unsupported default attribute value', stmt.rvalue.line)
# If the attribute is initialized to None and type isn't optional,
# don't initialize it to anything.
attr_type = cls.attr_type(lvalue.name)
if isinstance(stmt.rvalue, RefExpr) and stmt.rvalue.fullname == 'builtins.None':
if (not is_optional_type(attr_type) and not is_object_rprimitive(attr_type)
and not is_none_rprimitive(attr_type)):
continue
val = builder.coerce(builder.accept(stmt.rvalue), attr_type, stmt.line)
builder.add(SetAttr(self_var, lvalue.name, val, -1))
builder.add(Return(builder.true()))
def check_deletable_declaration(builder: IRBuilder, cl: ClassIR, line: int) -> None:
for attr in cl.deletable:
if attr not in cl.attributes:
if not cl.has_attr(attr):
builder.error('Attribute "{}" not defined'.format(attr), line)
continue
for base in cl.mro:
if attr in base.property_types:
builder.error('Cannot make property "{}" deletable'.format(attr), line)
break
else:
_, base = cl.attr_details(attr)
builder.error(('Attribute "{}" not defined in "{}" ' +
'(defined in "{}")').format(attr, cl.name, base.name), line)
def create_ne_from_eq(builder: IRBuilder, cdef: ClassDef) -> None:
"""Create a "__ne__" method from a "__eq__" method (if only latter exists)."""
cls = builder.mapper.type_to_ir[cdef.info]
if cls.has_method('__eq__') and not cls.has_method('__ne__'):
gen_glue_ne_method(builder, cls, cdef.line)
def gen_glue_ne_method(builder: IRBuilder, cls: ClassIR, line: int) -> None:
"""Generate a "__ne__" method from a "__eq__" method. """
with builder.enter_method(cls, '__ne__', object_rprimitive):
rhs_arg = builder.add_argument('rhs', object_rprimitive)
# If __eq__ returns NotImplemented, then __ne__ should also
not_implemented_block, regular_block = BasicBlock(), BasicBlock()
eqval = builder.add(MethodCall(builder.self(), '__eq__', [rhs_arg], line))
not_implemented = builder.add(LoadAddress(not_implemented_op.type,
not_implemented_op.src, line))
builder.add(Branch(
builder.translate_is_op(eqval, not_implemented, 'is', line),
not_implemented_block,
regular_block,
Branch.BOOL))
builder.activate_block(regular_block)
retval = builder.coerce(
builder.unary_op(eqval, 'not', line), object_rprimitive, line
)
builder.add(Return(retval))
builder.activate_block(not_implemented_block)
builder.add(Return(not_implemented))
def load_non_ext_class(builder: IRBuilder,
ir: ClassIR,
non_ext: NonExtClassInfo,
line: int) -> Value:
cls_name = builder.load_str(ir.name)
add_dunders_to_non_ext_dict(builder, non_ext, line)
class_type_obj = builder.py_call(
non_ext.metaclass,
[cls_name, non_ext.bases, non_ext.dict],
line
)
return class_type_obj
def load_decorated_class(builder: IRBuilder, cdef: ClassDef, type_obj: Value) -> Value:
"""Apply class decorators to create a decorated (non-extension) class object.
Given a decorated ClassDef and a register containing a
non-extension representation of the ClassDef created via the type
constructor, applies the corresponding decorator functions on that
decorated ClassDef and returns a register containing the decorated
ClassDef.
"""
decorators = cdef.decorators
dec_class = type_obj
for d in reversed(decorators):
decorator = d.accept(builder.visitor)
assert isinstance(decorator, Value)
dec_class = builder.py_call(decorator, [dec_class], dec_class.line)
return dec_class
def cache_class_attrs(builder: IRBuilder,
attrs_to_cache: List[Tuple[Lvalue, RType]],
cdef: ClassDef) -> None:
"""Add class attributes to be cached to the global cache."""
typ = builder.load_native_type_object(cdef.info.fullname)
for lval, rtype in attrs_to_cache:
assert isinstance(lval, NameExpr)
rval = builder.py_get_attr(typ, lval.name, cdef.line)
builder.init_final_static(lval, rval, cdef.name, type_override=rtype)
def create_mypyc_attrs_tuple(builder: IRBuilder, ir: ClassIR, line: int) -> Value:
attrs = [name for ancestor in ir.mro for name in ancestor.attributes]
if ir.inherits_python:
attrs.append('__dict__')
items = [builder.load_str(attr) for attr in attrs]
return builder.new_tuple(items, line)
def add_dunders_to_non_ext_dict(builder: IRBuilder, non_ext: NonExtClassInfo,
line: int, add_annotations: bool = True) -> None:
if add_annotations:
# Add __annotations__ to the class dict.
builder.add_to_non_ext_dict(non_ext, '__annotations__', non_ext.anns, line)
# We add a __doc__ attribute so if the non-extension class is decorated with the
# dataclass decorator, dataclass will not try to look for __text_signature__.
# https://github.com/python/cpython/blob/3.7/Lib/dataclasses.py#L957
filler_doc_str = 'mypyc filler docstring'
builder.add_to_non_ext_dict(
non_ext, '__doc__', builder.load_str(filler_doc_str), line)
builder.add_to_non_ext_dict(
non_ext, '__module__', builder.load_str(builder.module_name), line)

View file

@ -0,0 +1,99 @@
"""Constant folding of IR values.
For example, 3 + 5 can be constant folded into 8.
"""
from typing import Optional, Union
from typing_extensions import Final
from mypy.nodes import Expression, IntExpr, StrExpr, OpExpr, UnaryExpr, NameExpr, MemberExpr, Var
from mypyc.irbuild.builder import IRBuilder
# All possible result types of constant folding
ConstantValue = Union[int, str]
CONST_TYPES: Final = (int, str)
def constant_fold_expr(builder: IRBuilder, expr: Expression) -> Optional[ConstantValue]:
"""Return the constant value of an expression for supported operations.
Return None otherwise.
"""
if isinstance(expr, IntExpr):
return expr.value
if isinstance(expr, StrExpr):
return expr.value
elif isinstance(expr, NameExpr):
node = expr.node
if isinstance(node, Var) and node.is_final:
value = node.final_value
if isinstance(value, (CONST_TYPES)):
return value
elif isinstance(expr, MemberExpr):
final = builder.get_final_ref(expr)
if final is not None:
fn, final_var, native = final
if final_var.is_final:
value = final_var.final_value
if isinstance(value, (CONST_TYPES)):
return value
elif isinstance(expr, OpExpr):
left = constant_fold_expr(builder, expr.left)
right = constant_fold_expr(builder, expr.right)
if isinstance(left, int) and isinstance(right, int):
return constant_fold_binary_int_op(expr.op, left, right)
elif isinstance(left, str) and isinstance(right, str):
return constant_fold_binary_str_op(expr.op, left, right)
elif isinstance(expr, UnaryExpr):
value = constant_fold_expr(builder, expr.expr)
if isinstance(value, int):
return constant_fold_unary_int_op(expr.op, value)
return None
def constant_fold_binary_int_op(op: str, left: int, right: int) -> Optional[int]:
if op == '+':
return left + right
if op == '-':
return left - right
elif op == '*':
return left * right
elif op == '//':
if right != 0:
return left // right
elif op == '%':
if right != 0:
return left % right
elif op == '&':
return left & right
elif op == '|':
return left | right
elif op == '^':
return left ^ right
elif op == '<<':
if right >= 0:
return left << right
elif op == '>>':
if right >= 0:
return left >> right
elif op == '**':
if right >= 0:
return left ** right
return None
def constant_fold_unary_int_op(op: str, value: int) -> Optional[int]:
if op == '-':
return -value
elif op == '~':
return ~value
elif op == '+':
return value
return None
def constant_fold_binary_str_op(op: str, left: str, right: str) -> Optional[str]:
if op == '+':
return left + right
return None

View file

@ -0,0 +1,183 @@
"""Helpers that store information about functions and the related classes."""
from typing import List, Optional, Tuple
from mypy.nodes import FuncItem
from mypyc.ir.ops import Value, BasicBlock
from mypyc.ir.func_ir import INVALID_FUNC_DEF
from mypyc.ir.class_ir import ClassIR
from mypyc.irbuild.targets import AssignmentTarget
class FuncInfo:
"""Contains information about functions as they are generated."""
def __init__(self,
fitem: FuncItem = INVALID_FUNC_DEF,
name: str = '',
class_name: Optional[str] = None,
namespace: str = '',
is_nested: bool = False,
contains_nested: bool = False,
is_decorated: bool = False,
in_non_ext: bool = False) -> None:
self.fitem = fitem
self.name = name
self.class_name = class_name
self.ns = namespace
# Callable classes implement the '__call__' method, and are used to represent functions
# that are nested inside of other functions.
self._callable_class: Optional[ImplicitClass] = None
# Environment classes are ClassIR instances that contain attributes representing the
# variables in the environment of the function they correspond to. Environment classes are
# generated for functions that contain nested functions.
self._env_class: Optional[ClassIR] = None
# Generator classes implement the '__next__' method, and are used to represent generators
# returned by generator functions.
self._generator_class: Optional[GeneratorClass] = None
# Environment class registers are the local registers associated with instances of an
# environment class, used for getting and setting attributes. curr_env_reg is the register
# associated with the current environment.
self._curr_env_reg: Optional[Value] = None
# These are flags denoting whether a given function is nested, contains a nested function,
# is decorated, or is within a non-extension class.
self.is_nested = is_nested
self.contains_nested = contains_nested
self.is_decorated = is_decorated
self.in_non_ext = in_non_ext
# TODO: add field for ret_type: RType = none_rprimitive
def namespaced_name(self) -> str:
return '_'.join(x for x in [self.name, self.class_name, self.ns] if x)
@property
def is_generator(self) -> bool:
return self.fitem.is_generator or self.fitem.is_coroutine
@property
def is_coroutine(self) -> bool:
return self.fitem.is_coroutine
@property
def callable_class(self) -> 'ImplicitClass':
assert self._callable_class is not None
return self._callable_class
@callable_class.setter
def callable_class(self, cls: 'ImplicitClass') -> None:
self._callable_class = cls
@property
def env_class(self) -> ClassIR:
assert self._env_class is not None
return self._env_class
@env_class.setter
def env_class(self, ir: ClassIR) -> None:
self._env_class = ir
@property
def generator_class(self) -> 'GeneratorClass':
assert self._generator_class is not None
return self._generator_class
@generator_class.setter
def generator_class(self, cls: 'GeneratorClass') -> None:
self._generator_class = cls
@property
def curr_env_reg(self) -> Value:
assert self._curr_env_reg is not None
return self._curr_env_reg
class ImplicitClass:
"""Contains information regarding implicitly generated classes.
Implicit classes are generated for nested functions and generator
functions. They are not explicitly defined in the source code.
NOTE: This is both a concrete class and used as a base class.
"""
def __init__(self, ir: ClassIR) -> None:
# The ClassIR instance associated with this class.
self.ir = ir
# The register associated with the 'self' instance for this generator class.
self._self_reg: Optional[Value] = None
# Environment class registers are the local registers associated with instances of an
# environment class, used for getting and setting attributes. curr_env_reg is the register
# associated with the current environment. prev_env_reg is the self.__mypyc_env__ field
# associated with the previous environment.
self._curr_env_reg: Optional[Value] = None
self._prev_env_reg: Optional[Value] = None
@property
def self_reg(self) -> Value:
assert self._self_reg is not None
return self._self_reg
@self_reg.setter
def self_reg(self, reg: Value) -> None:
self._self_reg = reg
@property
def curr_env_reg(self) -> Value:
assert self._curr_env_reg is not None
return self._curr_env_reg
@curr_env_reg.setter
def curr_env_reg(self, reg: Value) -> None:
self._curr_env_reg = reg
@property
def prev_env_reg(self) -> Value:
assert self._prev_env_reg is not None
return self._prev_env_reg
@prev_env_reg.setter
def prev_env_reg(self, reg: Value) -> None:
self._prev_env_reg = reg
class GeneratorClass(ImplicitClass):
"""Contains information about implicit generator function classes."""
def __init__(self, ir: ClassIR) -> None:
super().__init__(ir)
# This register holds the label number that the '__next__' function should go to the next
# time it is called.
self._next_label_reg: Optional[Value] = None
self._next_label_target: Optional[AssignmentTarget] = None
# These registers hold the error values for the generator object for the case that the
# 'throw' function is called.
self.exc_regs: Optional[Tuple[Value, Value, Value]] = None
# Holds the arg passed to send
self.send_arg_reg: Optional[Value] = None
# The switch block is used to decide which instruction to go using the value held in the
# next-label register.
self.switch_block = BasicBlock()
self.continuation_blocks: List[BasicBlock] = []
@property
def next_label_reg(self) -> Value:
assert self._next_label_reg is not None
return self._next_label_reg
@next_label_reg.setter
def next_label_reg(self, reg: Value) -> None:
self._next_label_reg = reg
@property
def next_label_target(self) -> AssignmentTarget:
assert self._next_label_target is not None
return self._next_label_target
@next_label_target.setter
def next_label_target(self, target: AssignmentTarget) -> None:
self._next_label_target = target

View file

@ -0,0 +1,207 @@
"""Generate classes representing function environments (+ related operations).
If we have a nested function that has non-local (free) variables, access to the
non-locals is via an instance of an environment class. Example:
def f() -> int:
x = 0 # Make 'x' an attribute of an environment class instance
def g() -> int:
# We have access to the environment class instance to
# allow accessing 'x'
return x + 2
x = x + 1 # Modify the attribute
return g()
"""
from typing import Dict, Optional, Union
from mypy.nodes import FuncDef, SymbolNode
from mypyc.common import SELF_NAME, ENV_ATTR_NAME
from mypyc.ir.ops import Call, GetAttr, SetAttr, Value
from mypyc.ir.rtypes import RInstance, object_rprimitive
from mypyc.ir.class_ir import ClassIR
from mypyc.irbuild.builder import IRBuilder, SymbolTarget
from mypyc.irbuild.targets import AssignmentTargetAttr
from mypyc.irbuild.context import FuncInfo, ImplicitClass, GeneratorClass
def setup_env_class(builder: IRBuilder) -> ClassIR:
"""Generate a class representing a function environment.
Note that the variables in the function environment are not
actually populated here. This is because when the environment
class is generated, the function environment has not yet been
visited. This behavior is allowed so that when the compiler visits
nested functions, it can use the returned ClassIR instance to
figure out free variables it needs to access. The remaining
attributes of the environment class are populated when the
environment registers are loaded.
Return a ClassIR representing an environment for a function
containing a nested function.
"""
env_class = ClassIR('{}_env'.format(builder.fn_info.namespaced_name()),
builder.module_name, is_generated=True)
env_class.attributes[SELF_NAME] = RInstance(env_class)
if builder.fn_info.is_nested:
# If the function is nested, its environment class must contain an environment
# attribute pointing to its encapsulating functions' environment class.
env_class.attributes[ENV_ATTR_NAME] = RInstance(builder.fn_infos[-2].env_class)
env_class.mro = [env_class]
builder.fn_info.env_class = env_class
builder.classes.append(env_class)
return env_class
def finalize_env_class(builder: IRBuilder) -> None:
"""Generate, instantiate, and set up the environment of an environment class."""
instantiate_env_class(builder)
# Iterate through the function arguments and replace local definitions (using registers)
# that were previously added to the environment with references to the function's
# environment class.
if builder.fn_info.is_nested:
add_args_to_env(builder, local=False, base=builder.fn_info.callable_class)
else:
add_args_to_env(builder, local=False, base=builder.fn_info)
def instantiate_env_class(builder: IRBuilder) -> Value:
"""Assign an environment class to a register named after the given function definition."""
curr_env_reg = builder.add(
Call(builder.fn_info.env_class.ctor, [], builder.fn_info.fitem.line)
)
if builder.fn_info.is_nested:
builder.fn_info.callable_class._curr_env_reg = curr_env_reg
builder.add(SetAttr(curr_env_reg,
ENV_ATTR_NAME,
builder.fn_info.callable_class.prev_env_reg,
builder.fn_info.fitem.line))
else:
builder.fn_info._curr_env_reg = curr_env_reg
return curr_env_reg
def load_env_registers(builder: IRBuilder) -> None:
"""Load the registers for the current FuncItem being visited.
Adds the arguments of the FuncItem to the environment. If the
FuncItem is nested inside of another function, then this also
loads all of the outer environments of the FuncItem into registers
so that they can be used when accessing free variables.
"""
add_args_to_env(builder, local=True)
fn_info = builder.fn_info
fitem = fn_info.fitem
if fn_info.is_nested:
load_outer_envs(builder, fn_info.callable_class)
# If this is a FuncDef, then make sure to load the FuncDef into its own environment
# class so that the function can be called recursively.
if isinstance(fitem, FuncDef):
setup_func_for_recursive_call(builder, fitem, fn_info.callable_class)
def load_outer_env(builder: IRBuilder,
base: Value,
outer_env: Dict[SymbolNode, SymbolTarget]) -> Value:
"""Load the environment class for a given base into a register.
Additionally, iterates through all of the SymbolNode and
AssignmentTarget instances of the environment at the given index's
symtable, and adds those instances to the environment of the
current environment. This is done so that the current environment
can access outer environment variables without having to reload
all of the environment registers.
Returns the register where the environment class was loaded.
"""
env = builder.add(GetAttr(base, ENV_ATTR_NAME, builder.fn_info.fitem.line))
assert isinstance(env.type, RInstance), '{} must be of type RInstance'.format(env)
for symbol, target in outer_env.items():
env.type.class_ir.attributes[symbol.name] = target.type
symbol_target = AssignmentTargetAttr(env, symbol.name)
builder.add_target(symbol, symbol_target)
return env
def load_outer_envs(builder: IRBuilder, base: ImplicitClass) -> None:
index = len(builder.builders) - 2
# Load the first outer environment. This one is special because it gets saved in the
# FuncInfo instance's prev_env_reg field.
if index > 1:
# outer_env = builder.fn_infos[index].environment
outer_env = builder.symtables[index]
if isinstance(base, GeneratorClass):
base.prev_env_reg = load_outer_env(builder, base.curr_env_reg, outer_env)
else:
base.prev_env_reg = load_outer_env(builder, base.self_reg, outer_env)
env_reg = base.prev_env_reg
index -= 1
# Load the remaining outer environments into registers.
while index > 1:
# outer_env = builder.fn_infos[index].environment
outer_env = builder.symtables[index]
env_reg = load_outer_env(builder, env_reg, outer_env)
index -= 1
def add_args_to_env(builder: IRBuilder,
local: bool = True,
base: Optional[Union[FuncInfo, ImplicitClass]] = None,
reassign: bool = True) -> None:
fn_info = builder.fn_info
if local:
for arg in fn_info.fitem.arguments:
rtype = builder.type_to_rtype(arg.variable.type)
builder.add_local_reg(arg.variable, rtype, is_arg=True)
else:
for arg in fn_info.fitem.arguments:
if is_free_variable(builder, arg.variable) or fn_info.is_generator:
rtype = builder.type_to_rtype(arg.variable.type)
assert base is not None, 'base cannot be None for adding nonlocal args'
builder.add_var_to_env_class(arg.variable, rtype, base, reassign=reassign)
def setup_func_for_recursive_call(builder: IRBuilder, fdef: FuncDef, base: ImplicitClass) -> None:
"""Enable calling a nested function (with a callable class) recursively.
Adds the instance of the callable class representing the given
FuncDef to a register in the environment so that the function can
be called recursively. Note that this needs to be done only for
nested functions.
"""
# First, set the attribute of the environment class so that GetAttr can be called on it.
prev_env = builder.fn_infos[-2].env_class
prev_env.attributes[fdef.name] = builder.type_to_rtype(fdef.type)
if isinstance(base, GeneratorClass):
# If we are dealing with a generator class, then we need to first get the register
# holding the current environment class, and load the previous environment class from
# there.
prev_env_reg = builder.add(GetAttr(base.curr_env_reg, ENV_ATTR_NAME, -1))
else:
prev_env_reg = base.prev_env_reg
# Obtain the instance of the callable class representing the FuncDef, and add it to the
# current environment.
val = builder.add(GetAttr(prev_env_reg, fdef.name, -1))
target = builder.add_local_reg(fdef, object_rprimitive)
builder.assign(target, val, -1)
def is_free_variable(builder: IRBuilder, symbol: SymbolNode) -> bool:
fitem = builder.fn_info.fitem
return (
fitem in builder.free_variables
and symbol in builder.free_variables[fitem]
)

View file

@ -0,0 +1,808 @@
"""Transform mypy expression ASTs to mypyc IR (Intermediate Representation).
The top-level AST transformation logic is implemented in mypyc.irbuild.visitor
and mypyc.irbuild.builder.
"""
from typing import List, Optional, Union, Callable, cast
from mypy.nodes import (
Expression, NameExpr, MemberExpr, SuperExpr, CallExpr, UnaryExpr, OpExpr, IndexExpr,
ConditionalExpr, ComparisonExpr, IntExpr, FloatExpr, ComplexExpr, StrExpr,
BytesExpr, EllipsisExpr, ListExpr, TupleExpr, DictExpr, SetExpr, ListComprehension,
SetComprehension, DictionaryComprehension, SliceExpr, GeneratorExpr, CastExpr, StarExpr,
AssignmentExpr,
Var, RefExpr, MypyFile, TypeInfo, TypeApplication, LDEF, ARG_POS
)
from mypy.types import TupleType, Instance, TypeType, ProperType, get_proper_type
from mypyc.common import MAX_SHORT_INT
from mypyc.ir.ops import (
Value, Register, TupleGet, TupleSet, BasicBlock, Assign, LoadAddress, RaiseStandardError
)
from mypyc.ir.rtypes import (
RTuple, object_rprimitive, is_none_rprimitive, int_rprimitive, is_int_rprimitive
)
from mypyc.ir.func_ir import FUNC_CLASSMETHOD, FUNC_STATICMETHOD
from mypyc.irbuild.format_str_tokenizer import (
tokenizer_printf_style, join_formatted_strings, convert_format_expr_to_str,
convert_format_expr_to_bytes, join_formatted_bytes
)
from mypyc.primitives.bytes_ops import bytes_slice_op
from mypyc.primitives.registry import CFunctionDescription, builtin_names
from mypyc.primitives.generic_ops import iter_op
from mypyc.primitives.misc_ops import new_slice_op, ellipsis_op, type_op, get_module_dict_op
from mypyc.primitives.list_ops import list_append_op, list_extend_op, list_slice_op
from mypyc.primitives.tuple_ops import list_tuple_op, tuple_slice_op
from mypyc.primitives.dict_ops import dict_new_op, dict_set_item_op, dict_get_item_op
from mypyc.primitives.set_ops import set_add_op, set_update_op
from mypyc.primitives.str_ops import str_slice_op
from mypyc.primitives.int_ops import int_comparison_op_mapping
from mypyc.irbuild.specialize import apply_function_specialization, apply_method_specialization
from mypyc.irbuild.builder import IRBuilder
from mypyc.irbuild.for_helpers import (
translate_list_comprehension, translate_set_comprehension,
comprehension_helper
)
from mypyc.irbuild.constant_fold import constant_fold_expr
# Name and attribute references
def transform_name_expr(builder: IRBuilder, expr: NameExpr) -> Value:
if expr.node is None:
builder.add(RaiseStandardError(RaiseStandardError.RUNTIME_ERROR,
"mypyc internal error: should be unreachable",
expr.line))
return builder.none()
fullname = expr.node.fullname
if fullname in builtin_names:
typ, src = builtin_names[fullname]
return builder.add(LoadAddress(typ, src, expr.line))
# special cases
if fullname == 'builtins.None':
return builder.none()
if fullname == 'builtins.True':
return builder.true()
if fullname == 'builtins.False':
return builder.false()
if isinstance(expr.node, Var) and expr.node.is_final:
value = builder.emit_load_final(
expr.node,
fullname,
expr.name,
builder.is_native_ref_expr(expr),
builder.types[expr],
expr.line,
)
if value is not None:
return value
if isinstance(expr.node, MypyFile) and expr.node.fullname in builder.imports:
return builder.load_module(expr.node.fullname)
# If the expression is locally defined, then read the result from the corresponding
# assignment target and return it. Otherwise if the expression is a global, load it from
# the globals dictionary.
# Except for imports, that currently always happens in the global namespace.
if expr.kind == LDEF and not (isinstance(expr.node, Var)
and expr.node.is_suppressed_import):
# Try to detect and error when we hit the irritating mypy bug
# where a local variable is cast to None. (#5423)
if (isinstance(expr.node, Var) and is_none_rprimitive(builder.node_type(expr))
and expr.node.is_inferred):
builder.error(
'Local variable "{}" has inferred type None; add an annotation'.format(
expr.node.name),
expr.node.line)
# TODO: Behavior currently only defined for Var, FuncDef and MypyFile node types.
if isinstance(expr.node, MypyFile):
# Load reference to a module imported inside function from
# the modules dictionary. It would be closer to Python
# semantics to access modules imported inside functions
# via local variables, but this is tricky since the mypy
# AST doesn't include a Var node for the module. We
# instead load the module separately on each access.
mod_dict = builder.call_c(get_module_dict_op, [], expr.line)
obj = builder.call_c(dict_get_item_op,
[mod_dict, builder.load_str(expr.node.fullname)],
expr.line)
return obj
else:
return builder.read(builder.get_assignment_target(expr), expr.line)
return builder.load_global(expr)
def transform_member_expr(builder: IRBuilder, expr: MemberExpr) -> Value:
# First check if this is maybe a final attribute.
final = builder.get_final_ref(expr)
if final is not None:
fullname, final_var, native = final
value = builder.emit_load_final(final_var, fullname, final_var.name, native,
builder.types[expr], expr.line)
if value is not None:
return value
if isinstance(expr.node, MypyFile) and expr.node.fullname in builder.imports:
return builder.load_module(expr.node.fullname)
obj = builder.accept(expr.expr)
rtype = builder.node_type(expr)
# Special case: for named tuples transform attribute access to faster index access.
typ = get_proper_type(builder.types.get(expr.expr))
if isinstance(typ, TupleType) and typ.partial_fallback.type.is_named_tuple:
fields = typ.partial_fallback.type.metadata['namedtuple']['fields']
if expr.name in fields:
index = builder.builder.load_int(fields.index(expr.name))
return builder.gen_method_call(obj, '__getitem__', [index], rtype, expr.line)
check_instance_attribute_access_through_class(builder, expr, typ)
return builder.builder.get_attr(obj, expr.name, rtype, expr.line)
def check_instance_attribute_access_through_class(builder: IRBuilder,
expr: MemberExpr,
typ: Optional[ProperType]) -> None:
"""Report error if accessing an instance attribute through class object."""
if isinstance(expr.expr, RefExpr):
node = expr.expr.node
if isinstance(typ, TypeType) and isinstance(typ.item, Instance):
# TODO: Handle other item types
node = typ.item.type
if isinstance(node, TypeInfo):
class_ir = builder.mapper.type_to_ir.get(node)
if class_ir is not None and class_ir.is_ext_class:
sym = node.get(expr.name)
if (sym is not None
and isinstance(sym.node, Var)
and not sym.node.is_classvar
and not sym.node.is_final):
builder.error(
'Cannot access instance attribute "{}" through class object'.format(
expr.name),
expr.line
)
builder.note(
'(Hint: Use "x: Final = ..." or "x: ClassVar = ..." to define '
'a class attribute)',
expr.line
)
def transform_super_expr(builder: IRBuilder, o: SuperExpr) -> Value:
# warning(builder, 'can not optimize super() expression', o.line)
sup_val = builder.load_module_attr_by_fullname('builtins.super', o.line)
if o.call.args:
args = [builder.accept(arg) for arg in o.call.args]
else:
assert o.info is not None
typ = builder.load_native_type_object(o.info.fullname)
ir = builder.mapper.type_to_ir[o.info]
iter_env = iter(builder.builder.args)
# Grab first argument
vself: Value = next(iter_env)
if builder.fn_info.is_generator:
# grab sixth argument (see comment in translate_super_method_call)
self_targ = list(builder.symtables[-1].values())[6]
vself = builder.read(self_targ, builder.fn_info.fitem.line)
elif not ir.is_ext_class:
vself = next(iter_env) # second argument is self if non_extension class
args = [typ, vself]
res = builder.py_call(sup_val, args, o.line)
return builder.py_get_attr(res, o.name, o.line)
# Calls
def transform_call_expr(builder: IRBuilder, expr: CallExpr) -> Value:
if isinstance(expr.analyzed, CastExpr):
return translate_cast_expr(builder, expr.analyzed)
callee = expr.callee
if isinstance(callee, IndexExpr) and isinstance(callee.analyzed, TypeApplication):
callee = callee.analyzed.expr # Unwrap type application
if isinstance(callee, MemberExpr):
return apply_method_specialization(builder, expr, callee) or \
translate_method_call(builder, expr, callee)
elif isinstance(callee, SuperExpr):
return translate_super_method_call(builder, expr, callee)
else:
return translate_call(builder, expr, callee)
def translate_call(builder: IRBuilder, expr: CallExpr, callee: Expression) -> Value:
# The common case of calls is refexprs
if isinstance(callee, RefExpr):
return apply_function_specialization(builder, expr, callee) or \
translate_refexpr_call(builder, expr, callee)
function = builder.accept(callee)
args = [builder.accept(arg) for arg in expr.args]
return builder.py_call(function, args, expr.line,
arg_kinds=expr.arg_kinds, arg_names=expr.arg_names)
def translate_refexpr_call(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Value:
"""Translate a non-method call."""
# Gen the argument values
arg_values = [builder.accept(arg) for arg in expr.args]
return builder.call_refexpr_with_args(expr, callee, arg_values)
def translate_method_call(builder: IRBuilder, expr: CallExpr, callee: MemberExpr) -> Value:
"""Generate IR for an arbitrary call of form e.m(...).
This can also deal with calls to module-level functions.
"""
if builder.is_native_ref_expr(callee):
# Call to module-level native function or such
return translate_call(builder, expr, callee)
elif (
isinstance(callee.expr, RefExpr)
and isinstance(callee.expr.node, TypeInfo)
and callee.expr.node in builder.mapper.type_to_ir
and builder.mapper.type_to_ir[callee.expr.node].has_method(callee.name)
):
# Call a method via the *class*
assert isinstance(callee.expr.node, TypeInfo)
ir = builder.mapper.type_to_ir[callee.expr.node]
decl = ir.method_decl(callee.name)
args = []
arg_kinds, arg_names = expr.arg_kinds[:], expr.arg_names[:]
# Add the class argument for class methods in extension classes
if decl.kind == FUNC_CLASSMETHOD and ir.is_ext_class:
args.append(builder.load_native_type_object(callee.expr.node.fullname))
arg_kinds.insert(0, ARG_POS)
arg_names.insert(0, None)
args += [builder.accept(arg) for arg in expr.args]
if ir.is_ext_class:
return builder.builder.call(decl, args, arg_kinds, arg_names, expr.line)
else:
obj = builder.accept(callee.expr)
return builder.gen_method_call(obj,
callee.name,
args,
builder.node_type(expr),
expr.line,
expr.arg_kinds,
expr.arg_names)
elif builder.is_module_member_expr(callee):
# Fall back to a PyCall for non-native module calls
function = builder.accept(callee)
args = [builder.accept(arg) for arg in expr.args]
return builder.py_call(function, args, expr.line,
arg_kinds=expr.arg_kinds, arg_names=expr.arg_names)
else:
receiver_typ = builder.node_type(callee.expr)
# If there is a specializer for this method name/type, try calling it.
# We would return the first successful one.
val = apply_method_specialization(builder, expr, callee, receiver_typ)
if val is not None:
return val
obj = builder.accept(callee.expr)
args = [builder.accept(arg) for arg in expr.args]
return builder.gen_method_call(obj,
callee.name,
args,
builder.node_type(expr),
expr.line,
expr.arg_kinds,
expr.arg_names)
def translate_super_method_call(builder: IRBuilder, expr: CallExpr, callee: SuperExpr) -> Value:
if callee.info is None or (len(callee.call.args) != 0 and len(callee.call.args) != 2):
return translate_call(builder, expr, callee)
# We support two-argument super but only when it is super(CurrentClass, self)
# TODO: We could support it when it is a parent class in many cases?
if len(callee.call.args) == 2:
self_arg = callee.call.args[1]
if (
not isinstance(self_arg, NameExpr)
or not isinstance(self_arg.node, Var)
or not self_arg.node.is_self
):
return translate_call(builder, expr, callee)
typ_arg = callee.call.args[0]
if (
not isinstance(typ_arg, NameExpr)
or not isinstance(typ_arg.node, TypeInfo)
or callee.info is not typ_arg.node
):
return translate_call(builder, expr, callee)
ir = builder.mapper.type_to_ir[callee.info]
# Search for the method in the mro, skipping ourselves. We
# determine targets of super calls to native methods statically.
for base in ir.mro[1:]:
if callee.name in base.method_decls:
break
else:
if (ir.is_ext_class
and ir.builtin_base is None
and not ir.inherits_python
and callee.name == '__init__'
and len(expr.args) == 0):
# Call translates to object.__init__(self), which is a
# no-op, so omit the call.
return builder.none()
return translate_call(builder, expr, callee)
decl = base.method_decl(callee.name)
arg_values = [builder.accept(arg) for arg in expr.args]
arg_kinds, arg_names = expr.arg_kinds[:], expr.arg_names[:]
if decl.kind != FUNC_STATICMETHOD:
# Grab first argument
vself: Value = builder.self()
if decl.kind == FUNC_CLASSMETHOD:
vself = builder.call_c(type_op, [vself], expr.line)
elif builder.fn_info.is_generator:
# For generator classes, the self target is the 6th value
# in the symbol table (which is an ordered dict). This is sort
# of ugly, but we can't search by name since the 'self' parameter
# could be named anything, and it doesn't get added to the
# environment indexes.
self_targ = list(builder.symtables[-1].values())[6]
vself = builder.read(self_targ, builder.fn_info.fitem.line)
arg_values.insert(0, vself)
arg_kinds.insert(0, ARG_POS)
arg_names.insert(0, None)
return builder.builder.call(decl, arg_values, arg_kinds, arg_names, expr.line)
def translate_cast_expr(builder: IRBuilder, expr: CastExpr) -> Value:
src = builder.accept(expr.expr)
target_type = builder.type_to_rtype(expr.type)
return builder.coerce(src, target_type, expr.line)
# Operators
def transform_unary_expr(builder: IRBuilder, expr: UnaryExpr) -> Value:
folded = try_constant_fold(builder, expr)
if folded:
return folded
return builder.unary_op(builder.accept(expr.expr), expr.op, expr.line)
def transform_op_expr(builder: IRBuilder, expr: OpExpr) -> Value:
if expr.op in ('and', 'or'):
return builder.shortcircuit_expr(expr)
# Special case for string formatting
if expr.op == '%' and (isinstance(expr.left, StrExpr) or isinstance(expr.left, BytesExpr)):
ret = translate_printf_style_formatting(builder, expr.left, expr.right)
if ret is not None:
return ret
folded = try_constant_fold(builder, expr)
if folded:
return folded
return builder.binary_op(
builder.accept(expr.left), builder.accept(expr.right), expr.op, expr.line
)
def transform_index_expr(builder: IRBuilder, expr: IndexExpr) -> Value:
base = builder.accept(expr.base)
index = expr.index
if isinstance(base.type, RTuple) and isinstance(index, IntExpr):
return builder.add(TupleGet(base, index.value, expr.line))
if isinstance(index, SliceExpr):
value = try_gen_slice_op(builder, base, index)
if value:
return value
index_reg = builder.accept(expr.index)
return builder.gen_method_call(
base, '__getitem__', [index_reg], builder.node_type(expr), expr.line)
def try_constant_fold(builder: IRBuilder, expr: Expression) -> Optional[Value]:
"""Return the constant value of an expression if possible.
Return None otherwise.
"""
value = constant_fold_expr(builder, expr)
if isinstance(value, int):
return builder.load_int(value)
elif isinstance(value, str):
return builder.load_str(value)
return None
def try_gen_slice_op(builder: IRBuilder, base: Value, index: SliceExpr) -> Optional[Value]:
"""Generate specialized slice op for some index expressions.
Return None if a specialized op isn't available.
This supports obj[x:y], obj[:x], and obj[x:] for a few types.
"""
if index.stride:
# We can only handle the default stride of 1.
return None
if index.begin_index:
begin_type = builder.node_type(index.begin_index)
else:
begin_type = int_rprimitive
if index.end_index:
end_type = builder.node_type(index.end_index)
else:
end_type = int_rprimitive
# Both begin and end index must be int (or missing).
if is_int_rprimitive(begin_type) and is_int_rprimitive(end_type):
if index.begin_index:
begin = builder.accept(index.begin_index)
else:
begin = builder.load_int(0)
if index.end_index:
end = builder.accept(index.end_index)
else:
# Replace missing end index with the largest short integer
# (a sequence can't be longer).
end = builder.load_int(MAX_SHORT_INT)
candidates = [list_slice_op, tuple_slice_op, str_slice_op, bytes_slice_op]
return builder.builder.matching_call_c(candidates, [base, begin, end], index.line)
return None
def transform_conditional_expr(builder: IRBuilder, expr: ConditionalExpr) -> Value:
if_body, else_body, next_block = BasicBlock(), BasicBlock(), BasicBlock()
builder.process_conditional(expr.cond, if_body, else_body)
expr_type = builder.node_type(expr)
# Having actual Phi nodes would be really nice here!
target = Register(expr_type)
builder.activate_block(if_body)
true_value = builder.accept(expr.if_expr)
true_value = builder.coerce(true_value, expr_type, expr.line)
builder.add(Assign(target, true_value))
builder.goto(next_block)
builder.activate_block(else_body)
false_value = builder.accept(expr.else_expr)
false_value = builder.coerce(false_value, expr_type, expr.line)
builder.add(Assign(target, false_value))
builder.goto(next_block)
builder.activate_block(next_block)
return target
def transform_comparison_expr(builder: IRBuilder, e: ComparisonExpr) -> Value:
# x in (...)/[...]
# x not in (...)/[...]
if (e.operators[0] in ['in', 'not in']
and len(e.operators) == 1
and isinstance(e.operands[1], (TupleExpr, ListExpr))):
items = e.operands[1].items
n_items = len(items)
# x in y -> x == y[0] or ... or x == y[n]
# x not in y -> x != y[0] and ... and x != y[n]
# 16 is arbitrarily chosen to limit code size
if 1 < n_items < 16:
if e.operators[0] == 'in':
bin_op = 'or'
cmp_op = '=='
else:
bin_op = 'and'
cmp_op = '!='
lhs = e.operands[0]
mypy_file = builder.graph['builtins'].tree
assert mypy_file is not None
bool_type = Instance(cast(TypeInfo, mypy_file.names['bool'].node), [])
exprs = []
for item in items:
expr = ComparisonExpr([cmp_op], [lhs, item])
builder.types[expr] = bool_type
exprs.append(expr)
or_expr: Expression = exprs.pop(0)
for expr in exprs:
or_expr = OpExpr(bin_op, or_expr, expr)
builder.types[or_expr] = bool_type
return builder.accept(or_expr)
# x in [y]/(y) -> x == y
# x not in [y]/(y) -> x != y
elif n_items == 1:
if e.operators[0] == 'in':
cmp_op = '=='
else:
cmp_op = '!='
e.operators = [cmp_op]
e.operands[1] = items[0]
# x in []/() -> False
# x not in []/() -> True
elif n_items == 0:
if e.operators[0] == 'in':
return builder.false()
else:
return builder.true()
# TODO: Don't produce an expression when used in conditional context
# All of the trickiness here is due to support for chained conditionals
# (`e1 < e2 > e3`, etc). `e1 < e2 > e3` is approximately equivalent to
# `e1 < e2 and e2 > e3` except that `e2` is only evaluated once.
expr_type = builder.node_type(e)
# go(i, prev) generates code for `ei opi e{i+1} op{i+1} ... en`,
# assuming that prev contains the value of `ei`.
def go(i: int, prev: Value) -> Value:
if i == len(e.operators) - 1:
return transform_basic_comparison(
builder, e.operators[i], prev, builder.accept(e.operands[i + 1]), e.line)
next = builder.accept(e.operands[i + 1])
return builder.builder.shortcircuit_helper(
'and', expr_type,
lambda: transform_basic_comparison(
builder, e.operators[i], prev, next, e.line),
lambda: go(i + 1, next),
e.line)
return go(0, builder.accept(e.operands[0]))
def transform_basic_comparison(builder: IRBuilder,
op: str,
left: Value,
right: Value,
line: int) -> Value:
if (is_int_rprimitive(left.type) and is_int_rprimitive(right.type)
and op in int_comparison_op_mapping.keys()):
return builder.compare_tagged(left, right, op, line)
negate = False
if op == 'is not':
op, negate = 'is', True
elif op == 'not in':
op, negate = 'in', True
target = builder.binary_op(left, right, op, line)
if negate:
target = builder.unary_op(target, 'not', line)
return target
def translate_printf_style_formatting(builder: IRBuilder,
format_expr: Union[StrExpr, BytesExpr],
rhs: Expression) -> Optional[Value]:
tokens = tokenizer_printf_style(format_expr.value)
if tokens is not None:
literals, format_ops = tokens
exprs = []
if isinstance(rhs, TupleExpr):
exprs = rhs.items
elif isinstance(rhs, Expression):
exprs.append(rhs)
if isinstance(format_expr, BytesExpr):
substitutions = convert_format_expr_to_bytes(builder, format_ops,
exprs, format_expr.line)
if substitutions is not None:
return join_formatted_bytes(builder, literals, substitutions, format_expr.line)
else:
substitutions = convert_format_expr_to_str(builder, format_ops,
exprs, format_expr.line)
if substitutions is not None:
return join_formatted_strings(builder, literals, substitutions, format_expr.line)
return None
# Literals
def transform_int_expr(builder: IRBuilder, expr: IntExpr) -> Value:
return builder.builder.load_int(expr.value)
def transform_float_expr(builder: IRBuilder, expr: FloatExpr) -> Value:
return builder.builder.load_float(expr.value)
def transform_complex_expr(builder: IRBuilder, expr: ComplexExpr) -> Value:
return builder.builder.load_complex(expr.value)
def transform_str_expr(builder: IRBuilder, expr: StrExpr) -> Value:
return builder.load_str(expr.value)
def transform_bytes_expr(builder: IRBuilder, expr: BytesExpr) -> Value:
return builder.load_bytes_from_str_literal(expr.value)
def transform_ellipsis(builder: IRBuilder, o: EllipsisExpr) -> Value:
return builder.add(LoadAddress(ellipsis_op.type, ellipsis_op.src, o.line))
# Display expressions
def transform_list_expr(builder: IRBuilder, expr: ListExpr) -> Value:
return _visit_list_display(builder, expr.items, expr.line)
def _visit_list_display(builder: IRBuilder, items: List[Expression], line: int) -> Value:
return _visit_display(
builder,
items,
builder.new_list_op,
list_append_op,
list_extend_op,
line,
True
)
def transform_tuple_expr(builder: IRBuilder, expr: TupleExpr) -> Value:
if any(isinstance(item, StarExpr) for item in expr.items):
# create a tuple of unknown length
return _visit_tuple_display(builder, expr)
# create a tuple of fixed length (RTuple)
tuple_type = builder.node_type(expr)
# When handling NamedTuple et. al we might not have proper type info,
# so make some up if we need it.
types = (tuple_type.types if isinstance(tuple_type, RTuple)
else [object_rprimitive] * len(expr.items))
items = []
for item_expr, item_type in zip(expr.items, types):
reg = builder.accept(item_expr)
items.append(builder.coerce(reg, item_type, item_expr.line))
return builder.add(TupleSet(items, expr.line))
def _visit_tuple_display(builder: IRBuilder, expr: TupleExpr) -> Value:
"""Create a list, then turn it into a tuple."""
val_as_list = _visit_list_display(builder, expr.items, expr.line)
return builder.call_c(list_tuple_op, [val_as_list], expr.line)
def transform_dict_expr(builder: IRBuilder, expr: DictExpr) -> Value:
"""First accepts all keys and values, then makes a dict out of them."""
key_value_pairs = []
for key_expr, value_expr in expr.items:
key = builder.accept(key_expr) if key_expr is not None else None
value = builder.accept(value_expr)
key_value_pairs.append((key, value))
return builder.builder.make_dict(key_value_pairs, expr.line)
def transform_set_expr(builder: IRBuilder, expr: SetExpr) -> Value:
return _visit_display(
builder,
expr.items,
builder.new_set_op,
set_add_op,
set_update_op,
expr.line,
False
)
def _visit_display(builder: IRBuilder,
items: List[Expression],
constructor_op: Callable[[List[Value], int], Value],
append_op: CFunctionDescription,
extend_op: CFunctionDescription,
line: int,
is_list: bool
) -> Value:
accepted_items = []
for item in items:
if isinstance(item, StarExpr):
accepted_items.append((True, builder.accept(item.expr)))
else:
accepted_items.append((False, builder.accept(item)))
result: Union[Value, None] = None
initial_items = []
for starred, value in accepted_items:
if result is None and not starred and is_list:
initial_items.append(value)
continue
if result is None:
result = constructor_op(initial_items, line)
builder.call_c(extend_op if starred else append_op, [result, value], line)
if result is None:
result = constructor_op(initial_items, line)
return result
# Comprehensions
def transform_list_comprehension(builder: IRBuilder, o: ListComprehension) -> Value:
if any(o.generator.is_async):
builder.error('async comprehensions are unimplemented', o.line)
return translate_list_comprehension(builder, o.generator)
def transform_set_comprehension(builder: IRBuilder, o: SetComprehension) -> Value:
if any(o.generator.is_async):
builder.error('async comprehensions are unimplemented', o.line)
return translate_set_comprehension(builder, o.generator)
def transform_dictionary_comprehension(builder: IRBuilder, o: DictionaryComprehension) -> Value:
if any(o.is_async):
builder.error('async comprehensions are unimplemented', o.line)
d = builder.call_c(dict_new_op, [], o.line)
loop_params = list(zip(o.indices, o.sequences, o.condlists))
def gen_inner_stmts() -> None:
k = builder.accept(o.key)
v = builder.accept(o.value)
builder.call_c(dict_set_item_op, [d, k, v], o.line)
comprehension_helper(builder, loop_params, gen_inner_stmts, o.line)
return d
# Misc
def transform_slice_expr(builder: IRBuilder, expr: SliceExpr) -> Value:
def get_arg(arg: Optional[Expression]) -> Value:
if arg is None:
return builder.none_object()
else:
return builder.accept(arg)
args = [get_arg(expr.begin_index),
get_arg(expr.end_index),
get_arg(expr.stride)]
return builder.call_c(new_slice_op, args, expr.line)
def transform_generator_expr(builder: IRBuilder, o: GeneratorExpr) -> Value:
if any(o.is_async):
builder.error('async comprehensions are unimplemented', o.line)
builder.warning('Treating generator comprehension as list', o.line)
return builder.call_c(
iter_op, [translate_list_comprehension(builder, o)], o.line
)
def transform_assignment_expr(builder: IRBuilder, o: AssignmentExpr) -> Value:
value = builder.accept(o.value)
target = builder.get_assignment_target(o.target)
builder.assign(target, value, o.line)
return value

View file

@ -0,0 +1,880 @@
"""Helpers for generating for loops and comprehensions.
We special case certain kinds for loops such as "for x in range(...)"
for better efficiency. Each for loop generator class below deals one
such special case.
"""
from typing import Union, List, Optional, Tuple, Callable
from typing_extensions import Type, ClassVar
from mypy.nodes import (
Lvalue, Expression, TupleExpr, CallExpr, RefExpr, GeneratorExpr, ARG_POS, MemberExpr, TypeAlias
)
from mypyc.ir.ops import (
Value, BasicBlock, Integer, Branch, Register, TupleGet, TupleSet, IntOp
)
from mypyc.ir.rtypes import (
RType, is_short_int_rprimitive, is_list_rprimitive, is_sequence_rprimitive,
is_tuple_rprimitive, is_dict_rprimitive, is_str_rprimitive,
RTuple, short_int_rprimitive, int_rprimitive
)
from mypyc.primitives.registry import CFunctionDescription
from mypyc.primitives.dict_ops import (
dict_next_key_op, dict_next_value_op, dict_next_item_op, dict_check_size_op,
dict_key_iter_op, dict_value_iter_op, dict_item_iter_op
)
from mypyc.primitives.list_ops import list_append_op, list_get_item_unsafe_op, new_list_set_item_op
from mypyc.primitives.set_ops import set_add_op
from mypyc.primitives.generic_ops import iter_op, next_op
from mypyc.primitives.exc_ops import no_err_occurred_op
from mypyc.irbuild.builder import IRBuilder
from mypyc.irbuild.targets import AssignmentTarget, AssignmentTargetTuple
GenFunc = Callable[[], None]
def for_loop_helper(builder: IRBuilder, index: Lvalue, expr: Expression,
body_insts: GenFunc, else_insts: Optional[GenFunc],
line: int) -> None:
"""Generate IR for a loop.
Args:
index: the loop index Lvalue
expr: the expression to iterate over
body_insts: a function that generates the body of the loop
else_insts: a function that generates the else block instructions
"""
# Body of the loop
body_block = BasicBlock()
# Block that steps to the next item
step_block = BasicBlock()
# Block for the else clause, if we need it
else_block = BasicBlock()
# Block executed after the loop
exit_block = BasicBlock()
# Determine where we want to exit, if our condition check fails.
normal_loop_exit = else_block if else_insts is not None else exit_block
for_gen = make_for_loop_generator(builder, index, expr, body_block, normal_loop_exit, line)
builder.push_loop_stack(step_block, exit_block)
condition_block = BasicBlock()
builder.goto_and_activate(condition_block)
# Add loop condition check.
for_gen.gen_condition()
# Generate loop body.
builder.activate_block(body_block)
for_gen.begin_body()
body_insts()
# We generate a separate step block (which might be empty).
builder.goto_and_activate(step_block)
for_gen.gen_step()
# Go back to loop condition.
builder.goto(condition_block)
for_gen.add_cleanup(normal_loop_exit)
builder.pop_loop_stack()
if else_insts is not None:
builder.activate_block(else_block)
else_insts()
builder.goto(exit_block)
builder.activate_block(exit_block)
def for_loop_helper_with_index(builder: IRBuilder,
index: Lvalue,
expr: Expression,
expr_reg: Value,
body_insts: Callable[[Value], None], line: int) -> None:
"""Generate IR for a sequence iteration.
This function only works for sequence type. Compared to for_loop_helper,
it would feed iteration index to body_insts.
Args:
index: the loop index Lvalue
expr: the expression to iterate over
body_insts: a function that generates the body of the loop.
It needs a index as parameter.
"""
assert is_sequence_rprimitive(expr_reg.type)
target_type = builder.get_sequence_type(expr)
body_block = BasicBlock()
step_block = BasicBlock()
exit_block = BasicBlock()
condition_block = BasicBlock()
for_gen = ForSequence(builder, index, body_block, exit_block, line, False)
for_gen.init(expr_reg, target_type, reverse=False)
builder.push_loop_stack(step_block, exit_block)
builder.goto_and_activate(condition_block)
for_gen.gen_condition()
builder.activate_block(body_block)
for_gen.begin_body()
body_insts(builder.read(for_gen.index_target))
builder.goto_and_activate(step_block)
for_gen.gen_step()
builder.goto(condition_block)
for_gen.add_cleanup(exit_block)
builder.pop_loop_stack()
builder.activate_block(exit_block)
def sequence_from_generator_preallocate_helper(
builder: IRBuilder,
gen: GeneratorExpr,
empty_op_llbuilder: Callable[[Value, int], Value],
set_item_op: CFunctionDescription) -> Optional[Value]:
"""Generate a new tuple or list from a simple generator expression.
Currently we only optimize for simplest generator expression, which means that
there is no condition list in the generator and only one original sequence with
one index is allowed.
e.g. (1) tuple(f(x) for x in a_list/a_tuple)
(2) list(f(x) for x in a_list/a_tuple)
(3) [f(x) for x in a_list/a_tuple]
RTuple as an original sequence is not supported yet.
Args:
empty_op_llbuilder: A function that can generate an empty sequence op when
passed in length. See `new_list_op_with_length` and `new_tuple_op_with_length`
for detailed implementation.
set_item_op: A primitive that can modify an arbitrary position of a sequence.
The op should have three arguments:
- Self
- Target position
- New Value
See `new_list_set_item_op` and `new_tuple_set_item_op` for detailed
implementation.
"""
if len(gen.sequences) == 1 and len(gen.indices) == 1 and len(gen.condlists[0]) == 0:
rtype = builder.node_type(gen.sequences[0])
if (is_list_rprimitive(rtype) or is_tuple_rprimitive(rtype)
or is_str_rprimitive(rtype)):
sequence = builder.accept(gen.sequences[0])
length = builder.builder.builtin_len(sequence, gen.line, use_pyssize_t=True)
target_op = empty_op_llbuilder(length, gen.line)
def set_item(item_index: Value) -> None:
e = builder.accept(gen.left_expr)
builder.call_c(set_item_op, [target_op, item_index, e], gen.line)
for_loop_helper_with_index(builder, gen.indices[0], gen.sequences[0], sequence,
set_item, gen.line)
return target_op
return None
def translate_list_comprehension(builder: IRBuilder, gen: GeneratorExpr) -> Value:
# Try simplest list comprehension, otherwise fall back to general one
val = sequence_from_generator_preallocate_helper(
builder, gen,
empty_op_llbuilder=builder.builder.new_list_op_with_length,
set_item_op=new_list_set_item_op)
if val is not None:
return val
list_ops = builder.new_list_op([], gen.line)
loop_params = list(zip(gen.indices, gen.sequences, gen.condlists))
def gen_inner_stmts() -> None:
e = builder.accept(gen.left_expr)
builder.call_c(list_append_op, [list_ops, e], gen.line)
comprehension_helper(builder, loop_params, gen_inner_stmts, gen.line)
return list_ops
def translate_set_comprehension(builder: IRBuilder, gen: GeneratorExpr) -> Value:
set_ops = builder.new_set_op([], gen.line)
loop_params = list(zip(gen.indices, gen.sequences, gen.condlists))
def gen_inner_stmts() -> None:
e = builder.accept(gen.left_expr)
builder.call_c(set_add_op, [set_ops, e], gen.line)
comprehension_helper(builder, loop_params, gen_inner_stmts, gen.line)
return set_ops
def comprehension_helper(builder: IRBuilder,
loop_params: List[Tuple[Lvalue, Expression, List[Expression]]],
gen_inner_stmts: Callable[[], None],
line: int) -> None:
"""Helper function for list comprehensions.
Args:
loop_params: a list of (index, expr, [conditions]) tuples defining nested loops:
- "index" is the Lvalue indexing that loop;
- "expr" is the expression for the object to be iterated over;
- "conditions" is a list of conditions, evaluated in order with short-circuiting,
that must all be true for the loop body to be executed
gen_inner_stmts: function to generate the IR for the body of the innermost loop
"""
def handle_loop(loop_params: List[Tuple[Lvalue, Expression, List[Expression]]]) -> None:
"""Generate IR for a loop.
Given a list of (index, expression, [conditions]) tuples, generate IR
for the nested loops the list defines.
"""
index, expr, conds = loop_params[0]
for_loop_helper(builder, index, expr,
lambda: loop_contents(conds, loop_params[1:]),
None, line)
def loop_contents(
conds: List[Expression],
remaining_loop_params: List[Tuple[Lvalue, Expression, List[Expression]]],
) -> None:
"""Generate the body of the loop.
Args:
conds: a list of conditions to be evaluated (in order, with short circuiting)
to gate the body of the loop
remaining_loop_params: the parameters for any further nested loops; if it's empty
we'll instead evaluate the "gen_inner_stmts" function
"""
# Check conditions, in order, short circuiting them.
for cond in conds:
cond_val = builder.accept(cond)
cont_block, rest_block = BasicBlock(), BasicBlock()
# If the condition is true we'll skip the continue.
builder.add_bool_branch(cond_val, rest_block, cont_block)
builder.activate_block(cont_block)
builder.nonlocal_control[-1].gen_continue(builder, cond.line)
builder.goto_and_activate(rest_block)
if remaining_loop_params:
# There's another nested level, so the body of this loop is another loop.
return handle_loop(remaining_loop_params)
else:
# We finally reached the actual body of the generator.
# Generate the IR for the inner loop body.
gen_inner_stmts()
handle_loop(loop_params)
def is_range_ref(expr: RefExpr) -> bool:
return (expr.fullname == 'builtins.range'
or isinstance(expr.node, TypeAlias) and expr.fullname == 'six.moves.xrange')
def make_for_loop_generator(builder: IRBuilder,
index: Lvalue,
expr: Expression,
body_block: BasicBlock,
loop_exit: BasicBlock,
line: int,
nested: bool = False) -> 'ForGenerator':
"""Return helper object for generating a for loop over an iterable.
If "nested" is True, this is a nested iterator such as "e" in "enumerate(e)".
"""
rtyp = builder.node_type(expr)
if is_sequence_rprimitive(rtyp):
# Special case "for x in <list>".
expr_reg = builder.accept(expr)
target_type = builder.get_sequence_type(expr)
for_list = ForSequence(builder, index, body_block, loop_exit, line, nested)
for_list.init(expr_reg, target_type, reverse=False)
return for_list
if is_dict_rprimitive(rtyp):
# Special case "for k in <dict>".
expr_reg = builder.accept(expr)
target_type = builder.get_dict_key_type(expr)
for_dict = ForDictionaryKeys(builder, index, body_block, loop_exit, line, nested)
for_dict.init(expr_reg, target_type)
return for_dict
if (isinstance(expr, CallExpr)
and isinstance(expr.callee, RefExpr)):
if (is_range_ref(expr.callee)
and (len(expr.args) <= 2
or (len(expr.args) == 3
and builder.extract_int(expr.args[2]) is not None))
and set(expr.arg_kinds) == {ARG_POS}):
# Special case "for x in range(...)".
# We support the 3 arg form but only for int literals, since it doesn't
# seem worth the hassle of supporting dynamically determining which
# direction of comparison to do.
if len(expr.args) == 1:
start_reg: Value = Integer(0)
end_reg = builder.accept(expr.args[0])
else:
start_reg = builder.accept(expr.args[0])
end_reg = builder.accept(expr.args[1])
if len(expr.args) == 3:
step = builder.extract_int(expr.args[2])
assert step is not None
if step == 0:
builder.error("range() step can't be zero", expr.args[2].line)
else:
step = 1
for_range = ForRange(builder, index, body_block, loop_exit, line, nested)
for_range.init(start_reg, end_reg, step)
return for_range
elif (expr.callee.fullname == 'builtins.enumerate'
and len(expr.args) == 1
and expr.arg_kinds == [ARG_POS]
and isinstance(index, TupleExpr)
and len(index.items) == 2):
# Special case "for i, x in enumerate(y)".
lvalue1 = index.items[0]
lvalue2 = index.items[1]
for_enumerate = ForEnumerate(builder, index, body_block, loop_exit, line,
nested)
for_enumerate.init(lvalue1, lvalue2, expr.args[0])
return for_enumerate
elif (expr.callee.fullname == 'builtins.zip'
and len(expr.args) >= 2
and set(expr.arg_kinds) == {ARG_POS}
and isinstance(index, TupleExpr)
and len(index.items) == len(expr.args)):
# Special case "for x, y in zip(a, b)".
for_zip = ForZip(builder, index, body_block, loop_exit, line, nested)
for_zip.init(index.items, expr.args)
return for_zip
if (expr.callee.fullname == 'builtins.reversed'
and len(expr.args) == 1
and expr.arg_kinds == [ARG_POS]
and is_sequence_rprimitive(builder.node_type(expr.args[0]))):
# Special case "for x in reversed(<list>)".
expr_reg = builder.accept(expr.args[0])
target_type = builder.get_sequence_type(expr)
for_list = ForSequence(builder, index, body_block, loop_exit, line, nested)
for_list.init(expr_reg, target_type, reverse=True)
return for_list
if (isinstance(expr, CallExpr)
and isinstance(expr.callee, MemberExpr)
and not expr.args):
# Special cases for dictionary iterator methods, like dict.items().
rtype = builder.node_type(expr.callee.expr)
if (is_dict_rprimitive(rtype)
and expr.callee.name in ('keys', 'values', 'items')):
expr_reg = builder.accept(expr.callee.expr)
for_dict_type: Optional[Type[ForGenerator]] = None
if expr.callee.name == 'keys':
target_type = builder.get_dict_key_type(expr.callee.expr)
for_dict_type = ForDictionaryKeys
elif expr.callee.name == 'values':
target_type = builder.get_dict_value_type(expr.callee.expr)
for_dict_type = ForDictionaryValues
else:
target_type = builder.get_dict_item_type(expr.callee.expr)
for_dict_type = ForDictionaryItems
for_dict_gen = for_dict_type(builder, index, body_block, loop_exit, line, nested)
for_dict_gen.init(expr_reg, target_type)
return for_dict_gen
# Default to a generic for loop.
expr_reg = builder.accept(expr)
for_obj = ForIterable(builder, index, body_block, loop_exit, line, nested)
item_type = builder._analyze_iterable_item_type(expr)
item_rtype = builder.type_to_rtype(item_type)
for_obj.init(expr_reg, item_rtype)
return for_obj
class ForGenerator:
"""Abstract base class for generating for loops."""
def __init__(self,
builder: IRBuilder,
index: Lvalue,
body_block: BasicBlock,
loop_exit: BasicBlock,
line: int,
nested: bool) -> None:
self.builder = builder
self.index = index
self.body_block = body_block
self.line = line
# Some for loops need a cleanup block that we execute at exit. We
# create a cleanup block if needed. However, if we are generating a for
# loop for a nested iterator, such as "e" in "enumerate(e)", the
# outermost generator should generate the cleanup block -- we don't
# need to do it here.
if self.need_cleanup() and not nested:
# Create a new block to handle cleanup after loop exit.
self.loop_exit = BasicBlock()
else:
# Just use the existing loop exit block.
self.loop_exit = loop_exit
def need_cleanup(self) -> bool:
"""If this returns true, we need post-loop cleanup."""
return False
def add_cleanup(self, exit_block: BasicBlock) -> None:
"""Add post-loop cleanup, if needed."""
if self.need_cleanup():
self.builder.activate_block(self.loop_exit)
self.gen_cleanup()
self.builder.goto(exit_block)
def gen_condition(self) -> None:
"""Generate check for loop exit (e.g. exhaustion of iteration)."""
def begin_body(self) -> None:
"""Generate ops at the beginning of the body (if needed)."""
def gen_step(self) -> None:
"""Generate stepping to the next item (if needed)."""
def gen_cleanup(self) -> None:
"""Generate post-loop cleanup (if needed)."""
def load_len(self, expr: Union[Value, AssignmentTarget]) -> Value:
"""A helper to get collection length, used by several subclasses."""
return self.builder.builder.builtin_len(self.builder.read(expr, self.line), self.line)
class ForIterable(ForGenerator):
"""Generate IR for a for loop over an arbitrary iterable (the normal case)."""
def need_cleanup(self) -> bool:
# Create a new cleanup block for when the loop is finished.
return True
def init(self, expr_reg: Value, target_type: RType) -> None:
# Define targets to contain the expression, along with the iterator that will be used
# for the for-loop. If we are inside of a generator function, spill these into the
# environment class.
builder = self.builder
iter_reg = builder.call_c(iter_op, [expr_reg], self.line)
builder.maybe_spill(expr_reg)
self.iter_target = builder.maybe_spill(iter_reg)
self.target_type = target_type
def gen_condition(self) -> None:
# We call __next__ on the iterator and check to see if the return value
# is NULL, which signals either the end of the Iterable being traversed
# or an exception being raised. Note that Branch.IS_ERROR checks only
# for NULL (an exception does not necessarily have to be raised).
builder = self.builder
line = self.line
self.next_reg = builder.call_c(next_op, [builder.read(self.iter_target, line)], line)
builder.add(Branch(self.next_reg, self.loop_exit, self.body_block, Branch.IS_ERROR))
def begin_body(self) -> None:
# Assign the value obtained from __next__ to the
# lvalue so that it can be referenced by code in the body of the loop.
builder = self.builder
line = self.line
# We unbox here so that iterating with tuple unpacking generates a tuple based
# unpack instead of an iterator based one.
next_reg = builder.coerce(self.next_reg, self.target_type, line)
builder.assign(builder.get_assignment_target(self.index), next_reg, line)
def gen_step(self) -> None:
# Nothing to do here, since we get the next item as part of gen_condition().
pass
def gen_cleanup(self) -> None:
# We set the branch to go here if the conditional evaluates to true. If
# an exception was raised during the loop, then err_reg will be set to
# True. If no_err_occurred_op returns False, then the exception will be
# propagated using the ERR_FALSE flag.
self.builder.call_c(no_err_occurred_op, [], self.line)
def unsafe_index(
builder: IRBuilder, target: Value, index: Value, line: int
) -> Value:
"""Emit a potentially unsafe index into a target."""
# This doesn't really fit nicely into any of our data-driven frameworks
# since we want to use __getitem__ if we don't have an unsafe version,
# so we just check manually.
if is_list_rprimitive(target.type):
return builder.call_c(list_get_item_unsafe_op, [target, index], line)
else:
return builder.gen_method_call(target, '__getitem__', [index], None, line)
class ForSequence(ForGenerator):
"""Generate optimized IR for a for loop over a sequence.
Supports iterating in both forward and reverse.
"""
def init(self, expr_reg: Value, target_type: RType, reverse: bool) -> None:
builder = self.builder
self.reverse = reverse
# Define target to contain the expression, along with the index that will be used
# for the for-loop. If we are inside of a generator function, spill these into the
# environment class.
self.expr_target = builder.maybe_spill(expr_reg)
if not reverse:
index_reg: Value = Integer(0)
else:
index_reg = builder.binary_op(self.load_len(self.expr_target),
Integer(1), '-', self.line)
self.index_target = builder.maybe_spill_assignable(index_reg)
self.target_type = target_type
def gen_condition(self) -> None:
builder = self.builder
line = self.line
# TODO: Don't reload the length each time when iterating an immutable sequence?
if self.reverse:
# If we are iterating in reverse order, we obviously need
# to check that the index is still positive. Somewhat less
# obviously we still need to check against the length,
# since it could shrink out from under us.
comparison = builder.binary_op(builder.read(self.index_target, line),
Integer(0), '>=', line)
second_check = BasicBlock()
builder.add_bool_branch(comparison, second_check, self.loop_exit)
builder.activate_block(second_check)
# For compatibility with python semantics we recalculate the length
# at every iteration.
len_reg = self.load_len(self.expr_target)
comparison = builder.binary_op(builder.read(self.index_target, line), len_reg, '<', line)
builder.add_bool_branch(comparison, self.body_block, self.loop_exit)
def begin_body(self) -> None:
builder = self.builder
line = self.line
# Read the next list item.
value_box = unsafe_index(
builder,
builder.read(self.expr_target, line),
builder.read(self.index_target, line),
line
)
assert value_box
# We coerce to the type of list elements here so that
# iterating with tuple unpacking generates a tuple based
# unpack instead of an iterator based one.
builder.assign(builder.get_assignment_target(self.index),
builder.coerce(value_box, self.target_type, line), line)
def gen_step(self) -> None:
# Step to the next item.
builder = self.builder
line = self.line
step = 1 if not self.reverse else -1
add = builder.int_op(short_int_rprimitive,
builder.read(self.index_target, line),
Integer(step), IntOp.ADD, line)
builder.assign(self.index_target, add, line)
class ForDictionaryCommon(ForGenerator):
"""Generate optimized IR for a for loop over dictionary keys/values.
The logic is pretty straightforward, we use PyDict_Next() API wrapped in
a tuple, so that we can modify only a single register. The layout of the tuple:
* f0: are there more items (bool)
* f1: current offset (int)
* f2: next key (object)
* f3: next value (object)
For more info see https://docs.python.org/3/c-api/dict.html#c.PyDict_Next.
Note that for subclasses we fall back to generic PyObject_GetIter() logic,
since they may override some iteration methods in subtly incompatible manner.
The fallback logic is implemented in CPy.h via dynamic type check.
"""
dict_next_op: ClassVar[CFunctionDescription]
dict_iter_op: ClassVar[CFunctionDescription]
def need_cleanup(self) -> bool:
# Technically, a dict subclass can raise an unrelated exception
# in __next__(), so we need this.
return True
def init(self, expr_reg: Value, target_type: RType) -> None:
builder = self.builder
self.target_type = target_type
# We add some variables to environment class, so they can be read across yield.
self.expr_target = builder.maybe_spill(expr_reg)
offset = Integer(0)
self.offset_target = builder.maybe_spill_assignable(offset)
self.size = builder.maybe_spill(self.load_len(self.expr_target))
# For dict class (not a subclass) this is the dictionary itself.
iter_reg = builder.call_c(self.dict_iter_op, [expr_reg], self.line)
self.iter_target = builder.maybe_spill(iter_reg)
def gen_condition(self) -> None:
"""Get next key/value pair, set new offset, and check if we should continue."""
builder = self.builder
line = self.line
self.next_tuple = self.builder.call_c(
self.dict_next_op, [builder.read(self.iter_target, line),
builder.read(self.offset_target, line)], line)
# Do this here instead of in gen_step() to minimize variables in environment.
new_offset = builder.add(TupleGet(self.next_tuple, 1, line))
builder.assign(self.offset_target, new_offset, line)
should_continue = builder.add(TupleGet(self.next_tuple, 0, line))
builder.add(
Branch(should_continue, self.body_block, self.loop_exit, Branch.BOOL)
)
def gen_step(self) -> None:
"""Check that dictionary didn't change size during iteration.
Raise RuntimeError if it is not the case to match CPython behavior.
"""
builder = self.builder
line = self.line
# Technically, we don't need a new primitive for this, but it is simpler.
builder.call_c(dict_check_size_op,
[builder.read(self.expr_target, line),
builder.read(self.size, line)], line)
def gen_cleanup(self) -> None:
# Same as for generic ForIterable.
self.builder.call_c(no_err_occurred_op, [], self.line)
class ForDictionaryKeys(ForDictionaryCommon):
"""Generate optimized IR for a for loop over dictionary keys."""
dict_next_op = dict_next_key_op
dict_iter_op = dict_key_iter_op
def begin_body(self) -> None:
builder = self.builder
line = self.line
# Key is stored at the third place in the tuple.
key = builder.add(TupleGet(self.next_tuple, 2, line))
builder.assign(builder.get_assignment_target(self.index),
builder.coerce(key, self.target_type, line), line)
class ForDictionaryValues(ForDictionaryCommon):
"""Generate optimized IR for a for loop over dictionary values."""
dict_next_op = dict_next_value_op
dict_iter_op = dict_value_iter_op
def begin_body(self) -> None:
builder = self.builder
line = self.line
# Value is stored at the third place in the tuple.
value = builder.add(TupleGet(self.next_tuple, 2, line))
builder.assign(builder.get_assignment_target(self.index),
builder.coerce(value, self.target_type, line), line)
class ForDictionaryItems(ForDictionaryCommon):
"""Generate optimized IR for a for loop over dictionary items."""
dict_next_op = dict_next_item_op
dict_iter_op = dict_item_iter_op
def begin_body(self) -> None:
builder = self.builder
line = self.line
key = builder.add(TupleGet(self.next_tuple, 2, line))
value = builder.add(TupleGet(self.next_tuple, 3, line))
# Coerce just in case e.g. key is itself a tuple to be unpacked.
assert isinstance(self.target_type, RTuple)
key = builder.coerce(key, self.target_type.types[0], line)
value = builder.coerce(value, self.target_type.types[1], line)
target = builder.get_assignment_target(self.index)
if isinstance(target, AssignmentTargetTuple):
# Simpler code for common case: for k, v in d.items().
if len(target.items) != 2:
builder.error("Expected a pair for dict item iteration", line)
builder.assign(target.items[0], key, line)
builder.assign(target.items[1], value, line)
else:
rvalue = builder.add(TupleSet([key, value], line))
builder.assign(target, rvalue, line)
class ForRange(ForGenerator):
"""Generate optimized IR for a for loop over an integer range."""
def init(self, start_reg: Value, end_reg: Value, step: int) -> None:
builder = self.builder
self.start_reg = start_reg
self.end_reg = end_reg
self.step = step
self.end_target = builder.maybe_spill(end_reg)
if is_short_int_rprimitive(start_reg.type) and is_short_int_rprimitive(end_reg.type):
index_type = short_int_rprimitive
else:
index_type = int_rprimitive
index_reg = Register(index_type)
builder.assign(index_reg, start_reg, -1)
self.index_reg = builder.maybe_spill_assignable(index_reg)
# Initialize loop index to 0. Assert that the index target is assignable.
self.index_target: Union[Register, AssignmentTarget] = builder.get_assignment_target(
self.index
)
builder.assign(self.index_target, builder.read(self.index_reg, self.line), self.line)
def gen_condition(self) -> None:
builder = self.builder
line = self.line
# Add loop condition check.
cmp = '<' if self.step > 0 else '>'
comparison = builder.binary_op(builder.read(self.index_reg, line),
builder.read(self.end_target, line), cmp, line)
builder.add_bool_branch(comparison, self.body_block, self.loop_exit)
def gen_step(self) -> None:
builder = self.builder
line = self.line
# Increment index register. If the range is known to fit in short ints, use
# short ints.
if (is_short_int_rprimitive(self.start_reg.type)
and is_short_int_rprimitive(self.end_reg.type)):
new_val = builder.int_op(short_int_rprimitive,
builder.read(self.index_reg, line),
Integer(self.step), IntOp.ADD, line)
else:
new_val = builder.binary_op(
builder.read(self.index_reg, line), Integer(self.step), '+', line)
builder.assign(self.index_reg, new_val, line)
builder.assign(self.index_target, new_val, line)
class ForInfiniteCounter(ForGenerator):
"""Generate optimized IR for a for loop counting from 0 to infinity."""
def init(self) -> None:
builder = self.builder
# Create a register to store the state of the loop index and
# initialize this register along with the loop index to 0.
zero = Integer(0)
self.index_reg = builder.maybe_spill_assignable(zero)
self.index_target: Union[Register, AssignmentTarget] = builder.get_assignment_target(
self.index
)
builder.assign(self.index_target, zero, self.line)
def gen_step(self) -> None:
builder = self.builder
line = self.line
# We can safely assume that the integer is short, since we are not going to wrap
# around a 63-bit integer.
# NOTE: This would be questionable if short ints could be 32 bits.
new_val = builder.int_op(short_int_rprimitive,
builder.read(self.index_reg, line),
Integer(1), IntOp.ADD, line)
builder.assign(self.index_reg, new_val, line)
builder.assign(self.index_target, new_val, line)
class ForEnumerate(ForGenerator):
"""Generate optimized IR for a for loop of form "for i, x in enumerate(it)"."""
def need_cleanup(self) -> bool:
# The wrapped for loop might need cleanup. This might generate a
# redundant cleanup block, but that's okay.
return True
def init(self, index1: Lvalue, index2: Lvalue, expr: Expression) -> None:
# Count from 0 to infinity (for the index lvalue).
self.index_gen = ForInfiniteCounter(
self.builder,
index1,
self.body_block,
self.loop_exit,
self.line, nested=True)
self.index_gen.init()
# Iterate over the actual iterable.
self.main_gen = make_for_loop_generator(
self.builder,
index2,
expr,
self.body_block,
self.loop_exit,
self.line, nested=True)
def gen_condition(self) -> None:
# No need for a check for the index generator, since it's unconditional.
self.main_gen.gen_condition()
def begin_body(self) -> None:
self.index_gen.begin_body()
self.main_gen.begin_body()
def gen_step(self) -> None:
self.index_gen.gen_step()
self.main_gen.gen_step()
def gen_cleanup(self) -> None:
self.index_gen.gen_cleanup()
self.main_gen.gen_cleanup()
class ForZip(ForGenerator):
"""Generate IR for a for loop of form `for x, ... in zip(a, ...)`."""
def need_cleanup(self) -> bool:
# The wrapped for loops might need cleanup. We might generate a
# redundant cleanup block, but that's okay.
return True
def init(self, indexes: List[Lvalue], exprs: List[Expression]) -> None:
assert len(indexes) == len(exprs)
# Condition check will require multiple basic blocks, since there will be
# multiple conditions to check.
self.cond_blocks = [BasicBlock() for _ in range(len(indexes) - 1)] + [self.body_block]
self.gens: List[ForGenerator] = []
for index, expr, next_block in zip(indexes, exprs, self.cond_blocks):
gen = make_for_loop_generator(
self.builder,
index,
expr,
next_block,
self.loop_exit,
self.line, nested=True)
self.gens.append(gen)
def gen_condition(self) -> None:
for i, gen in enumerate(self.gens):
gen.gen_condition()
if i < len(self.gens) - 1:
self.builder.activate_block(self.cond_blocks[i])
def begin_body(self) -> None:
for gen in self.gens:
gen.begin_body()
def gen_step(self) -> None:
for gen in self.gens:
gen.gen_step()
def gen_cleanup(self) -> None:
for gen in self.gens:
gen.gen_cleanup()

View file

@ -0,0 +1,239 @@
"""Tokenizers for three string formatting methods"""
from typing import List, Tuple, Optional
from typing_extensions import Final
from enum import Enum, unique
from mypy.checkstrformat import (
parse_format_value, ConversionSpecifier, parse_conversion_specifiers
)
from mypy.errors import Errors
from mypy.messages import MessageBuilder
from mypy.nodes import Context, Expression
from mypyc.ir.ops import Value, Integer
from mypyc.ir.rtypes import (
c_pyssize_t_rprimitive, is_str_rprimitive, is_int_rprimitive, is_short_int_rprimitive,
is_bytes_rprimitive
)
from mypyc.irbuild.builder import IRBuilder
from mypyc.primitives.bytes_ops import bytes_build_op
from mypyc.primitives.int_ops import int_to_str_op
from mypyc.primitives.str_ops import str_build_op, str_op
@unique
class FormatOp(Enum):
"""FormatOp represents conversion operations of string formatting during
compile time.
Compare to ConversionSpecifier, FormatOp has fewer attributes.
For example, to mark a conversion from any object to string,
ConversionSpecifier may have several representations, like '%s', '{}'
or '{:{}}'. However, there would only exist one corresponding FormatOp.
"""
STR = 's'
INT = 'd'
BYTES = 'b'
def generate_format_ops(specifiers: List[ConversionSpecifier]) -> Optional[List[FormatOp]]:
"""Convert ConversionSpecifier to FormatOp.
Different ConversionSpecifiers may share a same FormatOp.
"""
format_ops = []
for spec in specifiers:
# TODO: Match specifiers instead of using whole_seq
if spec.whole_seq == '%s' or spec.whole_seq == '{:{}}':
format_op = FormatOp.STR
elif spec.whole_seq == '%d':
format_op = FormatOp.INT
elif spec.whole_seq == '%b':
format_op = FormatOp.BYTES
elif spec.whole_seq:
return None
else:
format_op = FormatOp.STR
format_ops.append(format_op)
return format_ops
def tokenizer_printf_style(format_str: str) -> Optional[Tuple[List[str], List[FormatOp]]]:
"""Tokenize a printf-style format string using regex.
Return:
A list of string literals and a list of FormatOps.
"""
literals: List[str] = []
specifiers: List[ConversionSpecifier] = parse_conversion_specifiers(format_str)
format_ops = generate_format_ops(specifiers)
if format_ops is None:
return None
last_end = 0
for spec in specifiers:
cur_start = spec.start_pos
literals.append(format_str[last_end:cur_start])
last_end = cur_start + len(spec.whole_seq)
literals.append(format_str[last_end:])
return literals, format_ops
# The empty Context as an argument for parse_format_value().
# It wouldn't be used since the code has passed the type-checking.
EMPTY_CONTEXT: Final = Context()
def tokenizer_format_call(
format_str: str) -> Optional[Tuple[List[str], List[FormatOp]]]:
"""Tokenize a str.format() format string.
The core function parse_format_value() is shared with mypy.
With these specifiers, we then parse the literal substrings
of the original format string and convert `ConversionSpecifier`
to `FormatOp`.
Return:
A list of string literals and a list of FormatOps. The literals
are interleaved with FormatOps and the length of returned literals
should be exactly one more than FormatOps.
Return None if it cannot parse the string.
"""
# Creates an empty MessageBuilder here.
# It wouldn't be used since the code has passed the type-checking.
specifiers = parse_format_value(format_str, EMPTY_CONTEXT,
MessageBuilder(Errors(), {}))
if specifiers is None:
return None
format_ops = generate_format_ops(specifiers)
if format_ops is None:
return None
literals: List[str] = []
last_end = 0
for spec in specifiers:
# Skip { and }
literals.append(format_str[last_end:spec.start_pos - 1])
last_end = spec.start_pos + len(spec.whole_seq) + 1
literals.append(format_str[last_end:])
# Deal with escaped {{
literals = [x.replace('{{', '{').replace('}}', '}') for x in literals]
return literals, format_ops
def convert_format_expr_to_str(builder: IRBuilder, format_ops: List[FormatOp],
exprs: List[Expression], line: int) -> Optional[List[Value]]:
"""Convert expressions into string literal objects with the guidance
of FormatOps. Return None when fails."""
if len(format_ops) != len(exprs):
return None
converted = []
for x, format_op in zip(exprs, format_ops):
node_type = builder.node_type(x)
if format_op == FormatOp.STR:
if is_str_rprimitive(node_type):
var_str = builder.accept(x)
elif is_int_rprimitive(node_type) or is_short_int_rprimitive(node_type):
var_str = builder.call_c(int_to_str_op, [builder.accept(x)], line)
else:
var_str = builder.call_c(str_op, [builder.accept(x)], line)
elif format_op == FormatOp.INT:
if is_int_rprimitive(node_type) or is_short_int_rprimitive(node_type):
var_str = builder.call_c(int_to_str_op, [builder.accept(x)], line)
else:
return None
else:
return None
converted.append(var_str)
return converted
def join_formatted_strings(builder: IRBuilder, literals: Optional[List[str]],
substitutions: List[Value], line: int) -> Value:
"""Merge the list of literals and the list of substitutions
alternatively using 'str_build_op'.
`substitutions` is the result value of formatting conversions.
If the `literals` is set to None, we simply join the substitutions;
Otherwise, the `literals` is the literal substrings of the original
format string and its length should be exactly one more than
substitutions.
For example:
(1) 'This is a %s and the value is %d'
-> literals: ['This is a ', ' and the value is', '']
(2) '{} and the value is {}'
-> literals: ['', ' and the value is', '']
"""
# The first parameter for str_build_op is the total size of
# the following PyObject*
result_list: List[Value] = [Integer(0, c_pyssize_t_rprimitive)]
if literals is not None:
for a, b in zip(literals, substitutions):
if a:
result_list.append(builder.load_str(a))
result_list.append(b)
if literals[-1]:
result_list.append(builder.load_str(literals[-1]))
else:
result_list.extend(substitutions)
# Special case for empty string and literal string
if len(result_list) == 1:
return builder.load_str("")
if not substitutions and len(result_list) == 2:
return result_list[1]
result_list[0] = Integer(len(result_list) - 1, c_pyssize_t_rprimitive)
return builder.call_c(str_build_op, result_list, line)
def convert_format_expr_to_bytes(builder: IRBuilder, format_ops: List[FormatOp],
exprs: List[Expression], line: int) -> Optional[List[Value]]:
"""Convert expressions into bytes literal objects with the guidance
of FormatOps. Return None when fails."""
if len(format_ops) != len(exprs):
return None
converted = []
for x, format_op in zip(exprs, format_ops):
node_type = builder.node_type(x)
# conversion type 's' is an alias of 'b' in bytes formatting
if format_op == FormatOp.BYTES or format_op == FormatOp.STR:
if is_bytes_rprimitive(node_type):
var_bytes = builder.accept(x)
else:
return None
else:
return None
converted.append(var_bytes)
return converted
def join_formatted_bytes(builder: IRBuilder, literals: List[str],
substitutions: List[Value], line: int) -> Value:
"""Merge the list of literals and the list of substitutions
alternatively using 'bytes_build_op'."""
result_list: List[Value] = [Integer(0, c_pyssize_t_rprimitive)]
for a, b in zip(literals, substitutions):
if a:
result_list.append(builder.load_bytes_from_str_literal(a))
result_list.append(b)
if literals[-1]:
result_list.append(builder.load_bytes_from_str_literal(literals[-1]))
# Special case for empty bytes and literal
if len(result_list) == 1:
return builder.load_bytes_from_str_literal('')
if not substitutions and len(result_list) == 2:
return result_list[1]
result_list[0] = Integer(len(result_list) - 1, c_pyssize_t_rprimitive)
return builder.call_c(bytes_build_op, result_list, line)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,272 @@
"""Generate IR for generator functions.
A generator function is represented by a class that implements the
generator protocol and keeps track of the generator state, including
local variables.
The top-level logic for dealing with generator functions is in
mypyc.irbuild.function.
"""
from typing import List
from mypy.nodes import Var, ARG_OPT
from mypyc.common import SELF_NAME, NEXT_LABEL_ATTR_NAME, ENV_ATTR_NAME
from mypyc.ir.ops import (
BasicBlock, Call, Return, Goto, Integer, SetAttr, Unreachable, RaiseStandardError,
Value, Register
)
from mypyc.ir.rtypes import RInstance, int_rprimitive, object_rprimitive
from mypyc.ir.func_ir import FuncIR, FuncDecl, FuncSignature, RuntimeArg
from mypyc.ir.class_ir import ClassIR
from mypyc.primitives.exc_ops import raise_exception_with_tb_op
from mypyc.irbuild.env_class import (
add_args_to_env, load_outer_env, load_env_registers, finalize_env_class
)
from mypyc.irbuild.builder import IRBuilder, gen_arg_defaults
from mypyc.irbuild.context import FuncInfo, GeneratorClass
def gen_generator_func(builder: IRBuilder) -> None:
setup_generator_class(builder)
load_env_registers(builder)
gen_arg_defaults(builder)
finalize_env_class(builder)
builder.add(Return(instantiate_generator_class(builder)))
def instantiate_generator_class(builder: IRBuilder) -> Value:
fitem = builder.fn_info.fitem
generator_reg = builder.add(Call(builder.fn_info.generator_class.ir.ctor, [], fitem.line))
# Get the current environment register. If the current function is nested, then the
# generator class gets instantiated from the callable class' '__call__' method, and hence
# we use the callable class' environment register. Otherwise, we use the original
# function's environment register.
if builder.fn_info.is_nested:
curr_env_reg = builder.fn_info.callable_class.curr_env_reg
else:
curr_env_reg = builder.fn_info.curr_env_reg
# Set the generator class' environment attribute to point at the environment class
# defined in the current scope.
builder.add(SetAttr(generator_reg, ENV_ATTR_NAME, curr_env_reg, fitem.line))
# Set the generator class' environment class' NEXT_LABEL_ATTR_NAME attribute to 0.
zero = Integer(0)
builder.add(SetAttr(curr_env_reg, NEXT_LABEL_ATTR_NAME, zero, fitem.line))
return generator_reg
def setup_generator_class(builder: IRBuilder) -> ClassIR:
name = '{}_gen'.format(builder.fn_info.namespaced_name())
generator_class_ir = ClassIR(name, builder.module_name, is_generated=True)
generator_class_ir.attributes[ENV_ATTR_NAME] = RInstance(builder.fn_info.env_class)
generator_class_ir.mro = [generator_class_ir]
builder.classes.append(generator_class_ir)
builder.fn_info.generator_class = GeneratorClass(generator_class_ir)
return generator_class_ir
def create_switch_for_generator_class(builder: IRBuilder) -> None:
builder.add(Goto(builder.fn_info.generator_class.switch_block))
block = BasicBlock()
builder.fn_info.generator_class.continuation_blocks.append(block)
builder.activate_block(block)
def populate_switch_for_generator_class(builder: IRBuilder) -> None:
cls = builder.fn_info.generator_class
line = builder.fn_info.fitem.line
builder.activate_block(cls.switch_block)
for label, true_block in enumerate(cls.continuation_blocks):
false_block = BasicBlock()
comparison = builder.binary_op(
cls.next_label_reg, Integer(label), '==', line
)
builder.add_bool_branch(comparison, true_block, false_block)
builder.activate_block(false_block)
builder.add(RaiseStandardError(RaiseStandardError.STOP_ITERATION, None, line))
builder.add(Unreachable())
def add_raise_exception_blocks_to_generator_class(builder: IRBuilder, line: int) -> None:
"""Add error handling blocks to a generator class.
Generates blocks to check if error flags are set while calling the
helper method for generator functions, and raises an exception if
those flags are set.
"""
cls = builder.fn_info.generator_class
assert cls.exc_regs is not None
exc_type, exc_val, exc_tb = cls.exc_regs
# Check to see if an exception was raised.
error_block = BasicBlock()
ok_block = BasicBlock()
comparison = builder.translate_is_op(exc_type, builder.none_object(), 'is not', line)
builder.add_bool_branch(comparison, error_block, ok_block)
builder.activate_block(error_block)
builder.call_c(raise_exception_with_tb_op, [exc_type, exc_val, exc_tb], line)
builder.add(Unreachable())
builder.goto_and_activate(ok_block)
def add_methods_to_generator_class(builder: IRBuilder,
fn_info: FuncInfo,
sig: FuncSignature,
arg_regs: List[Register],
blocks: List[BasicBlock],
is_coroutine: bool) -> None:
helper_fn_decl = add_helper_to_generator_class(builder, arg_regs, blocks, sig, fn_info)
add_next_to_generator_class(builder, fn_info, helper_fn_decl, sig)
add_send_to_generator_class(builder, fn_info, helper_fn_decl, sig)
add_iter_to_generator_class(builder, fn_info)
add_throw_to_generator_class(builder, fn_info, helper_fn_decl, sig)
add_close_to_generator_class(builder, fn_info)
if is_coroutine:
add_await_to_generator_class(builder, fn_info)
def add_helper_to_generator_class(builder: IRBuilder,
arg_regs: List[Register],
blocks: List[BasicBlock],
sig: FuncSignature,
fn_info: FuncInfo) -> FuncDecl:
"""Generates a helper method for a generator class, called by '__next__' and 'throw'."""
sig = FuncSignature((RuntimeArg(SELF_NAME, object_rprimitive),
RuntimeArg('type', object_rprimitive),
RuntimeArg('value', object_rprimitive),
RuntimeArg('traceback', object_rprimitive),
RuntimeArg('arg', object_rprimitive)
), sig.ret_type)
helper_fn_decl = FuncDecl('__mypyc_generator_helper__', fn_info.generator_class.ir.name,
builder.module_name, sig)
helper_fn_ir = FuncIR(helper_fn_decl, arg_regs, blocks,
fn_info.fitem.line, traceback_name=fn_info.fitem.name)
fn_info.generator_class.ir.methods['__mypyc_generator_helper__'] = helper_fn_ir
builder.functions.append(helper_fn_ir)
return helper_fn_decl
def add_iter_to_generator_class(builder: IRBuilder, fn_info: FuncInfo) -> None:
"""Generates the '__iter__' method for a generator class."""
with builder.enter_method(fn_info.generator_class.ir, '__iter__', object_rprimitive, fn_info):
builder.add(Return(builder.self()))
def add_next_to_generator_class(builder: IRBuilder,
fn_info: FuncInfo,
fn_decl: FuncDecl,
sig: FuncSignature) -> None:
"""Generates the '__next__' method for a generator class."""
with builder.enter_method(fn_info.generator_class.ir, '__next__',
object_rprimitive, fn_info):
none_reg = builder.none_object()
# Call the helper function with error flags set to Py_None, and return that result.
result = builder.add(Call(fn_decl,
[builder.self(), none_reg, none_reg, none_reg, none_reg],
fn_info.fitem.line))
builder.add(Return(result))
def add_send_to_generator_class(builder: IRBuilder,
fn_info: FuncInfo,
fn_decl: FuncDecl,
sig: FuncSignature) -> None:
"""Generates the 'send' method for a generator class."""
with builder.enter_method(fn_info.generator_class.ir, 'send', object_rprimitive, fn_info):
arg = builder.add_argument('arg', object_rprimitive)
none_reg = builder.none_object()
# Call the helper function with error flags set to Py_None, and return that result.
result = builder.add(Call(
fn_decl,
[builder.self(), none_reg, none_reg, none_reg, builder.read(arg)],
fn_info.fitem.line))
builder.add(Return(result))
def add_throw_to_generator_class(builder: IRBuilder,
fn_info: FuncInfo,
fn_decl: FuncDecl,
sig: FuncSignature) -> None:
"""Generates the 'throw' method for a generator class."""
with builder.enter_method(fn_info.generator_class.ir, 'throw',
object_rprimitive, fn_info):
typ = builder.add_argument('type', object_rprimitive)
val = builder.add_argument('value', object_rprimitive, ARG_OPT)
tb = builder.add_argument('traceback', object_rprimitive, ARG_OPT)
# Because the value and traceback arguments are optional and hence
# can be NULL if not passed in, we have to assign them Py_None if
# they are not passed in.
none_reg = builder.none_object()
builder.assign_if_null(val, lambda: none_reg, builder.fn_info.fitem.line)
builder.assign_if_null(tb, lambda: none_reg, builder.fn_info.fitem.line)
# Call the helper function using the arguments passed in, and return that result.
result = builder.add(Call(
fn_decl,
[builder.self(), builder.read(typ), builder.read(val), builder.read(tb), none_reg],
fn_info.fitem.line))
builder.add(Return(result))
def add_close_to_generator_class(builder: IRBuilder, fn_info: FuncInfo) -> None:
"""Generates the '__close__' method for a generator class."""
# TODO: Currently this method just triggers a runtime error.
# We should fill this out (https://github.com/mypyc/mypyc/issues/790).
with builder.enter_method(fn_info.generator_class.ir, 'close', object_rprimitive, fn_info):
builder.add(RaiseStandardError(RaiseStandardError.RUNTIME_ERROR,
'close method on generator classes unimplemented',
fn_info.fitem.line))
builder.add(Unreachable())
def add_await_to_generator_class(builder: IRBuilder, fn_info: FuncInfo) -> None:
"""Generates the '__await__' method for a generator class."""
with builder.enter_method(fn_info.generator_class.ir, '__await__', object_rprimitive, fn_info):
builder.add(Return(builder.self()))
def setup_env_for_generator_class(builder: IRBuilder) -> None:
"""Populates the environment for a generator class."""
fitem = builder.fn_info.fitem
cls = builder.fn_info.generator_class
self_target = builder.add_self_to_env(cls.ir)
# Add the type, value, and traceback variables to the environment.
exc_type = builder.add_local(Var('type'), object_rprimitive, is_arg=True)
exc_val = builder.add_local(Var('value'), object_rprimitive, is_arg=True)
exc_tb = builder.add_local(Var('traceback'), object_rprimitive, is_arg=True)
# TODO: Use the right type here instead of object?
exc_arg = builder.add_local(Var('arg'), object_rprimitive, is_arg=True)
cls.exc_regs = (exc_type, exc_val, exc_tb)
cls.send_arg_reg = exc_arg
cls.self_reg = builder.read(self_target, fitem.line)
cls.curr_env_reg = load_outer_env(builder, cls.self_reg, builder.symtables[-1])
# Define a variable representing the label to go to the next time
# the '__next__' function of the generator is called, and add it
# as an attribute to the environment class.
cls.next_label_target = builder.add_var_to_env_class(
Var(NEXT_LABEL_ATTR_NAME),
int_rprimitive,
cls,
reassign=False
)
# Add arguments from the original generator function to the
# environment of the generator class.
add_args_to_env(builder, local=False, base=cls, reassign=False)
# Set the next label register for the generator class.
cls.next_label_reg = builder.read(cls.next_label_target, fitem.line)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,134 @@
"""Transform a mypy AST to the IR form (Intermediate Representation).
For example, consider a function like this:
def f(x: int) -> int:
return x * 2 + 1
It would be translated to something that conceptually looks like this:
r0 = 2
r1 = 1
r2 = x * r0 :: int
r3 = r2 + r1 :: int
return r3
This module deals with the module-level IR transformation logic and
putting it all together. The actual IR is implemented in mypyc.ir.
For the core of the IR transform implementation, look at build_ir()
below, mypyc.irbuild.builder, and mypyc.irbuild.visitor.
"""
from mypy.backports import OrderedDict
from typing import List, Dict, Callable, Any, TypeVar, cast
from mypy.nodes import MypyFile, Expression, ClassDef
from mypy.types import Type
from mypy.state import strict_optional_set
from mypy.build import Graph
from mypyc.common import TOP_LEVEL_NAME
from mypyc.errors import Errors
from mypyc.options import CompilerOptions
from mypyc.ir.rtypes import none_rprimitive
from mypyc.ir.module_ir import ModuleIR, ModuleIRs
from mypyc.ir.func_ir import FuncIR, FuncDecl, FuncSignature
from mypyc.irbuild.prebuildvisitor import PreBuildVisitor
from mypyc.irbuild.vtable import compute_vtable
from mypyc.irbuild.prepare import build_type_map, find_singledispatch_register_impls
from mypyc.irbuild.builder import IRBuilder
from mypyc.irbuild.visitor import IRBuilderVisitor
from mypyc.irbuild.mapper import Mapper
# The stubs for callable contextmanagers are busted so cast it to the
# right type...
F = TypeVar('F', bound=Callable[..., Any])
strict_optional_dec = cast(Callable[[F], F], strict_optional_set(True))
@strict_optional_dec # Turn on strict optional for any type manipulations we do
def build_ir(modules: List[MypyFile],
graph: Graph,
types: Dict[Expression, Type],
mapper: 'Mapper',
options: CompilerOptions,
errors: Errors) -> ModuleIRs:
"""Build IR for a set of modules that have been type-checked by mypy."""
build_type_map(mapper, modules, graph, types, options, errors)
singledispatch_info = find_singledispatch_register_impls(modules, errors)
result: ModuleIRs = OrderedDict()
# Generate IR for all modules.
class_irs = []
for module in modules:
# First pass to determine free symbols.
pbv = PreBuildVisitor(errors, module, singledispatch_info.decorators_to_remove)
module.accept(pbv)
# Construct and configure builder objects (cyclic runtime dependency).
visitor = IRBuilderVisitor()
builder = IRBuilder(
module.fullname, types, graph, errors, mapper, pbv, visitor, options,
singledispatch_info.singledispatch_impls,
)
visitor.builder = builder
# Second pass does the bulk of the work.
transform_mypy_file(builder, module)
module_ir = ModuleIR(
module.fullname,
list(builder.imports),
builder.functions,
builder.classes,
builder.final_names
)
result[module.fullname] = module_ir
class_irs.extend(builder.classes)
# Compute vtables.
for cir in class_irs:
if cir.is_ext_class:
compute_vtable(cir)
return result
def transform_mypy_file(builder: IRBuilder, mypyfile: MypyFile) -> None:
"""Generate IR for a single module."""
if mypyfile.fullname in ('typing', 'abc'):
# These module are special; their contents are currently all
# built-in primitives.
return
builder.set_module(mypyfile.fullname, mypyfile.path)
classes = [node for node in mypyfile.defs if isinstance(node, ClassDef)]
# Collect all classes.
for cls in classes:
ir = builder.mapper.type_to_ir[cls.info]
builder.classes.append(ir)
builder.enter('<top level>')
# Make sure we have a builtins import
builder.gen_import('builtins', -1)
# Generate ops.
for node in mypyfile.defs:
builder.accept(node)
builder.maybe_add_implicit_return()
# Generate special function representing module top level.
args, _, blocks, ret_type, _ = builder.leave()
sig = FuncSignature([], none_rprimitive)
func_ir = FuncIR(FuncDecl(TOP_LEVEL_NAME, None, builder.module_name, sig), args, blocks,
traceback_name="<module>")
builder.functions.append(func_ir)

View file

@ -0,0 +1,162 @@
"""Maintain a mapping from mypy concepts to IR/compiled concepts."""
from typing import Dict, Optional
from mypy.nodes import FuncDef, TypeInfo, SymbolNode, ArgKind, ARG_STAR, ARG_STAR2
from mypy.types import (
Instance, Type, CallableType, LiteralType, TypedDictType, UnboundType, PartialType,
UninhabitedType, Overloaded, UnionType, TypeType, AnyType, NoneTyp, TupleType, TypeVarType,
get_proper_type
)
from mypyc.ir.rtypes import (
RType, RUnion, RTuple, RInstance, object_rprimitive, dict_rprimitive, tuple_rprimitive,
none_rprimitive, int_rprimitive, float_rprimitive, str_rprimitive, bool_rprimitive,
list_rprimitive, set_rprimitive, range_rprimitive, bytes_rprimitive
)
from mypyc.ir.func_ir import FuncSignature, FuncDecl, RuntimeArg
from mypyc.ir.class_ir import ClassIR
class Mapper:
"""Keep track of mappings from mypy concepts to IR concepts.
For example, we keep track of how the mypy TypeInfos of compiled
classes map to class IR objects.
This state is shared across all modules being compiled in all
compilation groups.
"""
def __init__(self, group_map: Dict[str, Optional[str]]) -> None:
self.group_map = group_map
self.type_to_ir: Dict[TypeInfo, ClassIR] = {}
self.func_to_decl: Dict[SymbolNode, FuncDecl] = {}
def type_to_rtype(self, typ: Optional[Type]) -> RType:
if typ is None:
return object_rprimitive
typ = get_proper_type(typ)
if isinstance(typ, Instance):
if typ.type.fullname == 'builtins.int':
return int_rprimitive
elif typ.type.fullname == 'builtins.float':
return float_rprimitive
elif typ.type.fullname == 'builtins.bool':
return bool_rprimitive
elif typ.type.fullname == 'builtins.str':
return str_rprimitive
elif typ.type.fullname == 'builtins.bytes':
return bytes_rprimitive
elif typ.type.fullname == 'builtins.list':
return list_rprimitive
# Dict subclasses are at least somewhat common and we
# specifically support them, so make sure that dict operations
# get optimized on them.
elif any(cls.fullname == 'builtins.dict' for cls in typ.type.mro):
return dict_rprimitive
elif typ.type.fullname == 'builtins.set':
return set_rprimitive
elif typ.type.fullname == 'builtins.tuple':
return tuple_rprimitive # Varying-length tuple
elif typ.type.fullname == 'builtins.range':
return range_rprimitive
elif typ.type in self.type_to_ir:
inst = RInstance(self.type_to_ir[typ.type])
# Treat protocols as Union[protocol, object], so that we can do fast
# method calls in the cases where the protocol is explicitly inherited from
# and fall back to generic operations when it isn't.
if typ.type.is_protocol:
return RUnion([inst, object_rprimitive])
else:
return inst
else:
return object_rprimitive
elif isinstance(typ, TupleType):
# Use our unboxed tuples for raw tuples but fall back to
# being boxed for NamedTuple.
if typ.partial_fallback.type.fullname == 'builtins.tuple':
return RTuple([self.type_to_rtype(t) for t in typ.items])
else:
return tuple_rprimitive
elif isinstance(typ, CallableType):
return object_rprimitive
elif isinstance(typ, NoneTyp):
return none_rprimitive
elif isinstance(typ, UnionType):
return RUnion([self.type_to_rtype(item)
for item in typ.items])
elif isinstance(typ, AnyType):
return object_rprimitive
elif isinstance(typ, TypeType):
return object_rprimitive
elif isinstance(typ, TypeVarType):
# Erase type variable to upper bound.
# TODO: Erase to union if object has value restriction?
return self.type_to_rtype(typ.upper_bound)
elif isinstance(typ, PartialType):
assert typ.var.type is not None
return self.type_to_rtype(typ.var.type)
elif isinstance(typ, Overloaded):
return object_rprimitive
elif isinstance(typ, TypedDictType):
return dict_rprimitive
elif isinstance(typ, LiteralType):
return self.type_to_rtype(typ.fallback)
elif isinstance(typ, (UninhabitedType, UnboundType)):
# Sure, whatever!
return object_rprimitive
# I think we've covered everything that is supposed to
# actually show up, so anything else is a bug somewhere.
assert False, 'unexpected type %s' % type(typ)
def get_arg_rtype(self, typ: Type, kind: ArgKind) -> RType:
if kind == ARG_STAR:
return tuple_rprimitive
elif kind == ARG_STAR2:
return dict_rprimitive
else:
return self.type_to_rtype(typ)
def fdef_to_sig(self, fdef: FuncDef) -> FuncSignature:
if isinstance(fdef.type, CallableType):
arg_types = [self.get_arg_rtype(typ, kind)
for typ, kind in zip(fdef.type.arg_types, fdef.type.arg_kinds)]
arg_pos_onlys = [name is None for name in fdef.type.arg_names]
ret = self.type_to_rtype(fdef.type.ret_type)
else:
# Handle unannotated functions
arg_types = [object_rprimitive for arg in fdef.arguments]
arg_pos_onlys = [arg.pos_only for arg in fdef.arguments]
# We at least know the return type for __init__ methods will be None.
is_init_method = fdef.name == '__init__' and bool(fdef.info)
if is_init_method:
ret = none_rprimitive
else:
ret = object_rprimitive
# mypyc FuncSignatures (unlike mypy types) want to have a name
# present even when the argument is position only, since it is
# the sole way that FuncDecl arguments are tracked. This is
# generally fine except in some cases (like for computing
# init_sig) we need to produce FuncSignatures from a
# deserialized FuncDef that lacks arguments. We won't ever
# need to use those inside of a FuncIR, so we just make up
# some crap.
if hasattr(fdef, 'arguments'):
arg_names = [arg.variable.name for arg in fdef.arguments]
else:
arg_names = [name or '' for name in fdef.arg_names]
args = [RuntimeArg(arg_name, arg_type, arg_kind, arg_pos_only)
for arg_name, arg_kind, arg_type, arg_pos_only
in zip(arg_names, fdef.arg_kinds, arg_types, arg_pos_onlys)]
# We force certain dunder methods to return objects to support letting them
# return NotImplemented. It also avoids some pointless boxing and unboxing,
# since tp_richcompare needs an object anyways.
if fdef.name in ('__eq__', '__ne__', '__lt__', '__gt__', '__le__', '__ge__'):
ret = object_rprimitive
return FuncSignature(args, ret)

View file

@ -0,0 +1,184 @@
"""Helpers for dealing with nonlocal control such as 'break' and 'return'.
Model how these behave differently in different contexts.
"""
from abc import abstractmethod
from typing import Optional, Union
from typing_extensions import TYPE_CHECKING
from mypyc.ir.ops import (
Branch, BasicBlock, Unreachable, Value, Goto, Integer, Assign, Register, Return,
NO_TRACEBACK_LINE_NO
)
from mypyc.primitives.exc_ops import set_stop_iteration_value, restore_exc_info_op
from mypyc.irbuild.targets import AssignmentTarget
if TYPE_CHECKING:
from mypyc.irbuild.builder import IRBuilder
class NonlocalControl:
"""ABC representing a stack frame of constructs that modify nonlocal control flow.
The nonlocal control flow constructs are break, continue, and
return, and their behavior is modified by a number of other
constructs. The most obvious is loop, which override where break
and continue jump to, but also `except` (which needs to clear
exc_info when left) and (eventually) finally blocks (which need to
ensure that the finally block is always executed when leaving the
try/except blocks).
"""
@abstractmethod
def gen_break(self, builder: 'IRBuilder', line: int) -> None: pass
@abstractmethod
def gen_continue(self, builder: 'IRBuilder', line: int) -> None: pass
@abstractmethod
def gen_return(self, builder: 'IRBuilder', value: Value, line: int) -> None: pass
class BaseNonlocalControl(NonlocalControl):
"""Default nonlocal control outside any statements that affect it."""
def gen_break(self, builder: 'IRBuilder', line: int) -> None:
assert False, "break outside of loop"
def gen_continue(self, builder: 'IRBuilder', line: int) -> None:
assert False, "continue outside of loop"
def gen_return(self, builder: 'IRBuilder', value: Value, line: int) -> None:
builder.add(Return(value))
class LoopNonlocalControl(NonlocalControl):
"""Nonlocal control within a loop."""
def __init__(self,
outer: NonlocalControl,
continue_block: BasicBlock,
break_block: BasicBlock) -> None:
self.outer = outer
self.continue_block = continue_block
self.break_block = break_block
def gen_break(self, builder: 'IRBuilder', line: int) -> None:
builder.add(Goto(self.break_block))
def gen_continue(self, builder: 'IRBuilder', line: int) -> None:
builder.add(Goto(self.continue_block))
def gen_return(self, builder: 'IRBuilder', value: Value, line: int) -> None:
self.outer.gen_return(builder, value, line)
class GeneratorNonlocalControl(BaseNonlocalControl):
"""Default nonlocal control in a generator function outside statements."""
def gen_return(self, builder: 'IRBuilder', value: Value, line: int) -> None:
# Assign an invalid next label number so that the next time
# __next__ is called, we jump to the case in which
# StopIteration is raised.
builder.assign(builder.fn_info.generator_class.next_label_target,
Integer(-1),
line)
# Raise a StopIteration containing a field for the value that
# should be returned. Before doing so, create a new block
# without an error handler set so that the implicitly thrown
# StopIteration isn't caught by except blocks inside of the
# generator function.
builder.builder.push_error_handler(None)
builder.goto_and_activate(BasicBlock())
# Skip creating a traceback frame when we raise here, because
# we don't care about the traceback frame and it is kind of
# expensive since raising StopIteration is an extremely common
# case. Also we call a special internal function to set
# StopIteration instead of using RaiseStandardError because
# the obvious thing doesn't work if the value is a tuple
# (???).
builder.call_c(set_stop_iteration_value, [value], NO_TRACEBACK_LINE_NO)
builder.add(Unreachable())
builder.builder.pop_error_handler()
class CleanupNonlocalControl(NonlocalControl):
"""Abstract nonlocal control that runs some cleanup code. """
def __init__(self, outer: NonlocalControl) -> None:
self.outer = outer
@abstractmethod
def gen_cleanup(self, builder: 'IRBuilder', line: int) -> None: ...
def gen_break(self, builder: 'IRBuilder', line: int) -> None:
self.gen_cleanup(builder, line)
self.outer.gen_break(builder, line)
def gen_continue(self, builder: 'IRBuilder', line: int) -> None:
self.gen_cleanup(builder, line)
self.outer.gen_continue(builder, line)
def gen_return(self, builder: 'IRBuilder', value: Value, line: int) -> None:
self.gen_cleanup(builder, line)
self.outer.gen_return(builder, value, line)
class TryFinallyNonlocalControl(NonlocalControl):
"""Nonlocal control within try/finally."""
def __init__(self, target: BasicBlock) -> None:
self.target = target
self.ret_reg: Optional[Register] = None
def gen_break(self, builder: 'IRBuilder', line: int) -> None:
builder.error("break inside try/finally block is unimplemented", line)
def gen_continue(self, builder: 'IRBuilder', line: int) -> None:
builder.error("continue inside try/finally block is unimplemented", line)
def gen_return(self, builder: 'IRBuilder', value: Value, line: int) -> None:
if self.ret_reg is None:
self.ret_reg = Register(builder.ret_types[-1])
builder.add(Assign(self.ret_reg, value))
builder.add(Goto(self.target))
class ExceptNonlocalControl(CleanupNonlocalControl):
"""Nonlocal control for except blocks.
Just makes sure that sys.exc_info always gets restored when we leave.
This is super annoying.
"""
def __init__(self, outer: NonlocalControl, saved: Union[Value, AssignmentTarget]) -> None:
super().__init__(outer)
self.saved = saved
def gen_cleanup(self, builder: 'IRBuilder', line: int) -> None:
builder.call_c(restore_exc_info_op, [builder.read(self.saved)], line)
class FinallyNonlocalControl(CleanupNonlocalControl):
"""Nonlocal control for finally blocks.
Just makes sure that sys.exc_info always gets restored when we
leave and the return register is decrefed if it isn't null.
"""
def __init__(self, outer: NonlocalControl, ret_reg: Optional[Value], saved: Value) -> None:
super().__init__(outer)
self.ret_reg = ret_reg
self.saved = saved
def gen_cleanup(self, builder: 'IRBuilder', line: int) -> None:
# Restore the old exc_info
target, cleanup = BasicBlock(), BasicBlock()
builder.add(Branch(self.saved, target, cleanup, Branch.IS_ERROR))
builder.activate_block(cleanup)
builder.call_c(restore_exc_info_op, [self.saved], line)
builder.goto_and_activate(target)

View file

@ -0,0 +1,168 @@
from mypyc.errors import Errors
from typing import Dict, List, Set
from mypy.nodes import (
Decorator, Expression, FuncDef, FuncItem, LambdaExpr, NameExpr, SymbolNode, Var, MemberExpr,
MypyFile
)
from mypy.traverser import TraverserVisitor
class PreBuildVisitor(TraverserVisitor):
"""Mypy file AST visitor run before building the IR.
This collects various things, including:
* Determine relationships between nested functions and functions that
contain nested functions
* Find non-local variables (free variables)
* Find property setters
* Find decorators of functions
The main IR build pass uses this information.
"""
def __init__(
self,
errors: Errors,
current_file: MypyFile,
decorators_to_remove: Dict[FuncDef, List[int]],
) -> None:
super().__init__()
# Dict from a function to symbols defined directly in the
# function that are used as non-local (free) variables within a
# nested function.
self.free_variables: Dict[FuncItem, Set[SymbolNode]] = {}
# Intermediate data structure used to find the function where
# a SymbolNode is declared. Initially this may point to a
# function nested inside the function with the declaration,
# but we'll eventually update this to refer to the function
# with the declaration.
self.symbols_to_funcs: Dict[SymbolNode, FuncItem] = {}
# Stack representing current function nesting.
self.funcs: List[FuncItem] = []
# All property setters encountered so far.
self.prop_setters: Set[FuncDef] = set()
# A map from any function that contains nested functions to
# a set of all the functions that are nested within it.
self.encapsulating_funcs: Dict[FuncItem, List[FuncItem]] = {}
# Map nested function to its parent/encapsulating function.
self.nested_funcs: Dict[FuncItem, FuncItem] = {}
# Map function to its non-special decorators.
self.funcs_to_decorators: Dict[FuncDef, List[Expression]] = {}
# Map function to indices of decorators to remove
self.decorators_to_remove: Dict[FuncDef, List[int]] = decorators_to_remove
self.errors: Errors = errors
self.current_file: MypyFile = current_file
def visit_decorator(self, dec: Decorator) -> None:
if dec.decorators:
# Only add the function being decorated if there exist
# (ordinary) decorators in the decorator list. Certain
# decorators (such as @property, @abstractmethod) are
# special cased and removed from this list by
# mypy. Functions decorated only by special decorators
# (and property setters) are not treated as decorated
# functions by the IR builder.
if isinstance(dec.decorators[0], MemberExpr) and dec.decorators[0].name == 'setter':
# Property setters are not treated as decorated methods.
self.prop_setters.add(dec.func)
else:
decorators_to_store = dec.decorators.copy()
if dec.func in self.decorators_to_remove:
to_remove = self.decorators_to_remove[dec.func]
for i in reversed(to_remove):
del decorators_to_store[i]
# if all of the decorators are removed, we shouldn't treat this as a decorated
# function because there aren't any decorators to apply
if not decorators_to_store:
return
self.funcs_to_decorators[dec.func] = decorators_to_store
super().visit_decorator(dec)
def visit_func_def(self, fdef: FuncItem) -> None:
# TODO: What about overloaded functions?
self.visit_func(fdef)
def visit_lambda_expr(self, expr: LambdaExpr) -> None:
self.visit_func(expr)
def visit_func(self, func: FuncItem) -> None:
# If there were already functions or lambda expressions
# defined in the function stack, then note the previous
# FuncItem as containing a nested function and the current
# FuncItem as being a nested function.
if self.funcs:
# Add the new func to the set of nested funcs within the
# func at top of the func stack.
self.encapsulating_funcs.setdefault(self.funcs[-1], []).append(func)
# Add the func at top of the func stack as the parent of
# new func.
self.nested_funcs[func] = self.funcs[-1]
self.funcs.append(func)
super().visit_func(func)
self.funcs.pop()
def visit_name_expr(self, expr: NameExpr) -> None:
if isinstance(expr.node, (Var, FuncDef)):
self.visit_symbol_node(expr.node)
def visit_var(self, var: Var) -> None:
self.visit_symbol_node(var)
def visit_symbol_node(self, symbol: SymbolNode) -> None:
if not self.funcs:
# We are not inside a function and hence do not need to do
# anything regarding free variables.
return
if symbol in self.symbols_to_funcs:
orig_func = self.symbols_to_funcs[symbol]
if self.is_parent(self.funcs[-1], orig_func):
# The function in which the symbol was previously seen is
# nested within the function currently being visited. Thus
# the current function is a better candidate to contain the
# declaration.
self.symbols_to_funcs[symbol] = self.funcs[-1]
# TODO: Remove from the orig_func free_variables set?
self.free_variables.setdefault(self.funcs[-1], set()).add(symbol)
elif self.is_parent(orig_func, self.funcs[-1]):
# The SymbolNode instance has already been visited
# before in a parent function, thus it's a non-local
# symbol.
self.add_free_variable(symbol)
else:
# This is the first time the SymbolNode is being
# visited. We map the SymbolNode to the current FuncDef
# being visited to note where it was first visited.
self.symbols_to_funcs[symbol] = self.funcs[-1]
def is_parent(self, fitem: FuncItem, child: FuncItem) -> bool:
# Check if child is nested within fdef (possibly indirectly
# within multiple nested functions).
if child in self.nested_funcs:
parent = self.nested_funcs[child]
if parent == fitem:
return True
return self.is_parent(fitem, parent)
return False
def add_free_variable(self, symbol: SymbolNode) -> None:
# Find the function where the symbol was (likely) first declared,
# and mark is as a non-local symbol within that function.
func = self.symbols_to_funcs[symbol]
self.free_variables.setdefault(func, set()).add(symbol)

View file

@ -0,0 +1,425 @@
"""Prepare for IR transform.
This needs to run after type checking and before generating IR.
For example, construct partially initialized FuncIR and ClassIR
objects for all functions and classes. This allows us to bind
references to functions and classes before we've generated full IR for
functions or classes. The actual IR transform will then populate all
the missing bits, such as function bodies (basic blocks).
Also build a mapping from mypy TypeInfos to ClassIR objects.
"""
from typing import List, Dict, Iterable, Optional, Union, DefaultDict, NamedTuple, Tuple
from mypy.nodes import (
ClassDef, OverloadedFuncDef, Var,
SymbolNode, ARG_STAR, ARG_STAR2, CallExpr, Decorator, Expression, FuncDef,
MemberExpr, MypyFile, NameExpr, RefExpr, TypeInfo
)
from mypy.types import Type, Instance, get_proper_type
from mypy.build import Graph
from mypyc.ir.ops import DeserMaps
from mypyc.ir.rtypes import RInstance, tuple_rprimitive, dict_rprimitive
from mypyc.ir.func_ir import (
FuncDecl, FuncSignature, RuntimeArg, FUNC_NORMAL, FUNC_STATICMETHOD, FUNC_CLASSMETHOD
)
from mypyc.ir.class_ir import ClassIR
from mypyc.common import PROPSET_PREFIX, get_id_from_name
from mypyc.irbuild.mapper import Mapper
from mypyc.irbuild.util import (
get_func_def, is_dataclass, is_trait, is_extension_class, get_mypyc_attrs
)
from mypyc.errors import Errors
from mypyc.options import CompilerOptions
from mypyc.crash import catch_errors
from collections import defaultdict
from mypy.traverser import TraverserVisitor
from mypy.semanal import refers_to_fullname
def build_type_map(mapper: Mapper,
modules: List[MypyFile],
graph: Graph,
types: Dict[Expression, Type],
options: CompilerOptions,
errors: Errors) -> None:
# Collect all classes defined in everything we are compiling
classes = []
for module in modules:
module_classes = [node for node in module.defs if isinstance(node, ClassDef)]
classes.extend([(module, cdef) for cdef in module_classes])
# Collect all class mappings so that we can bind arbitrary class name
# references even if there are import cycles.
for module, cdef in classes:
class_ir = ClassIR(cdef.name, module.fullname, is_trait(cdef),
is_abstract=cdef.info.is_abstract)
class_ir.is_ext_class = is_extension_class(cdef)
if class_ir.is_ext_class:
class_ir.deletable = cdef.info.deletable_attributes[:]
# If global optimizations are disabled, turn of tracking of class children
if not options.global_opts:
class_ir.children = None
mapper.type_to_ir[cdef.info] = class_ir
# Populate structural information in class IR for extension classes.
for module, cdef in classes:
with catch_errors(module.path, cdef.line):
if mapper.type_to_ir[cdef.info].is_ext_class:
prepare_class_def(module.path, module.fullname, cdef, errors, mapper)
else:
prepare_non_ext_class_def(module.path, module.fullname, cdef, errors, mapper)
# Collect all the functions also. We collect from the symbol table
# so that we can easily pick out the right copy of a function that
# is conditionally defined.
for module in modules:
for func in get_module_func_defs(module):
prepare_func_def(module.fullname, None, func, mapper)
# TODO: what else?
def is_from_module(node: SymbolNode, module: MypyFile) -> bool:
return node.fullname == module.fullname + '.' + node.name
def load_type_map(mapper: 'Mapper',
modules: List[MypyFile],
deser_ctx: DeserMaps) -> None:
"""Populate a Mapper with deserialized IR from a list of modules."""
for module in modules:
for name, node in module.names.items():
if isinstance(node.node, TypeInfo) and is_from_module(node.node, module):
ir = deser_ctx.classes[node.node.fullname]
mapper.type_to_ir[node.node] = ir
mapper.func_to_decl[node.node] = ir.ctor
for module in modules:
for func in get_module_func_defs(module):
func_id = get_id_from_name(func.name, func.fullname, func.line)
mapper.func_to_decl[func] = deser_ctx.functions[func_id].decl
def get_module_func_defs(module: MypyFile) -> Iterable[FuncDef]:
"""Collect all of the (non-method) functions declared in a module."""
for name, node in module.names.items():
# We need to filter out functions that are imported or
# aliases. The best way to do this seems to be by
# checking that the fullname matches.
if (isinstance(node.node, (FuncDef, Decorator, OverloadedFuncDef))
and is_from_module(node.node, module)):
yield get_func_def(node.node)
def prepare_func_def(module_name: str, class_name: Optional[str],
fdef: FuncDef, mapper: Mapper) -> FuncDecl:
kind = FUNC_STATICMETHOD if fdef.is_static else (
FUNC_CLASSMETHOD if fdef.is_class else FUNC_NORMAL)
decl = FuncDecl(fdef.name, class_name, module_name, mapper.fdef_to_sig(fdef), kind)
mapper.func_to_decl[fdef] = decl
return decl
def prepare_method_def(ir: ClassIR, module_name: str, cdef: ClassDef, mapper: Mapper,
node: Union[FuncDef, Decorator]) -> None:
if isinstance(node, FuncDef):
ir.method_decls[node.name] = prepare_func_def(module_name, cdef.name, node, mapper)
elif isinstance(node, Decorator):
# TODO: do something about abstract methods here. Currently, they are handled just like
# normal methods.
decl = prepare_func_def(module_name, cdef.name, node.func, mapper)
if not node.decorators:
ir.method_decls[node.name] = decl
elif isinstance(node.decorators[0], MemberExpr) and node.decorators[0].name == 'setter':
# Make property setter name different than getter name so there are no
# name clashes when generating C code, and property lookup at the IR level
# works correctly.
decl.name = PROPSET_PREFIX + decl.name
decl.is_prop_setter = True
ir.method_decls[PROPSET_PREFIX + node.name] = decl
if node.func.is_property:
assert node.func.type, f"Expected return type annotation for property '{node.name}'"
decl.is_prop_getter = True
ir.property_types[node.name] = decl.sig.ret_type
def is_valid_multipart_property_def(prop: OverloadedFuncDef) -> bool:
# Checks to ensure supported property decorator semantics
if len(prop.items) == 2:
getter = prop.items[0]
setter = prop.items[1]
if isinstance(getter, Decorator) and isinstance(setter, Decorator):
if getter.func.is_property and len(setter.decorators) == 1:
if isinstance(setter.decorators[0], MemberExpr):
if setter.decorators[0].name == "setter":
return True
return False
def can_subclass_builtin(builtin_base: str) -> bool:
# BaseException and dict are special cased.
return builtin_base in (
('builtins.Exception', 'builtins.LookupError', 'builtins.IndexError',
'builtins.Warning', 'builtins.UserWarning', 'builtins.ValueError',
'builtins.object', ))
def prepare_class_def(path: str, module_name: str, cdef: ClassDef,
errors: Errors, mapper: Mapper) -> None:
ir = mapper.type_to_ir[cdef.info]
info = cdef.info
attrs = get_mypyc_attrs(cdef)
if attrs.get("allow_interpreted_subclasses") is True:
ir.allow_interpreted_subclasses = True
# We sort the table for determinism here on Python 3.5
for name, node in sorted(info.names.items()):
# Currently all plugin generated methods are dummies and not included.
if node.plugin_generated:
continue
if isinstance(node.node, Var):
assert node.node.type, "Class member %s missing type" % name
if not node.node.is_classvar and name not in ('__slots__', '__deletable__'):
ir.attributes[name] = mapper.type_to_rtype(node.node.type)
elif isinstance(node.node, (FuncDef, Decorator)):
prepare_method_def(ir, module_name, cdef, mapper, node.node)
elif isinstance(node.node, OverloadedFuncDef):
# Handle case for property with both a getter and a setter
if node.node.is_property:
if is_valid_multipart_property_def(node.node):
for item in node.node.items:
prepare_method_def(ir, module_name, cdef, mapper, item)
else:
errors.error("Unsupported property decorator semantics", path, cdef.line)
# Handle case for regular function overload
else:
assert node.node.impl
prepare_method_def(ir, module_name, cdef, mapper, node.node.impl)
# Check for subclassing from builtin types
for cls in info.mro:
# Special case exceptions and dicts
# XXX: How do we handle *other* things??
if cls.fullname == 'builtins.BaseException':
ir.builtin_base = 'PyBaseExceptionObject'
elif cls.fullname == 'builtins.dict':
ir.builtin_base = 'PyDictObject'
elif cls.fullname.startswith('builtins.'):
if not can_subclass_builtin(cls.fullname):
# Note that if we try to subclass a C extension class that
# isn't in builtins, bad things will happen and we won't
# catch it here! But this should catch a lot of the most
# common pitfalls.
errors.error("Inheriting from most builtin types is unimplemented",
path, cdef.line)
if ir.builtin_base:
ir.attributes.clear()
# Set up a constructor decl
init_node = cdef.info['__init__'].node
if not ir.is_trait and not ir.builtin_base and isinstance(init_node, FuncDef):
init_sig = mapper.fdef_to_sig(init_node)
defining_ir = mapper.type_to_ir.get(init_node.info)
# If there is a nontrivial __init__ that wasn't defined in an
# extension class, we need to make the constructor take *args,
# **kwargs so it can call tp_init.
if ((defining_ir is None or not defining_ir.is_ext_class
or cdef.info['__init__'].plugin_generated)
and init_node.info.fullname != 'builtins.object'):
init_sig = FuncSignature(
[init_sig.args[0],
RuntimeArg("args", tuple_rprimitive, ARG_STAR),
RuntimeArg("kwargs", dict_rprimitive, ARG_STAR2)],
init_sig.ret_type)
ctor_sig = FuncSignature(init_sig.args[1:], RInstance(ir))
ir.ctor = FuncDecl(cdef.name, None, module_name, ctor_sig)
mapper.func_to_decl[cdef.info] = ir.ctor
# Set up the parent class
bases = [mapper.type_to_ir[base.type] for base in info.bases
if base.type in mapper.type_to_ir]
if not all(c.is_trait for c in bases[1:]):
errors.error("Non-trait bases must appear first in parent list", path, cdef.line)
ir.traits = [c for c in bases if c.is_trait]
mro = []
base_mro = []
for cls in info.mro:
if cls not in mapper.type_to_ir:
if cls.fullname != 'builtins.object':
ir.inherits_python = True
continue
base_ir = mapper.type_to_ir[cls]
if not base_ir.is_trait:
base_mro.append(base_ir)
mro.append(base_ir)
if cls.defn.removed_base_type_exprs or not base_ir.is_ext_class:
ir.inherits_python = True
base_idx = 1 if not ir.is_trait else 0
if len(base_mro) > base_idx:
ir.base = base_mro[base_idx]
ir.mro = mro
ir.base_mro = base_mro
for base in bases:
if base.children is not None:
base.children.append(ir)
if is_dataclass(cdef):
ir.is_augmented = True
def prepare_non_ext_class_def(path: str, module_name: str, cdef: ClassDef,
errors: Errors, mapper: Mapper) -> None:
ir = mapper.type_to_ir[cdef.info]
info = cdef.info
for name, node in info.names.items():
if isinstance(node.node, (FuncDef, Decorator)):
prepare_method_def(ir, module_name, cdef, mapper, node.node)
elif isinstance(node.node, OverloadedFuncDef):
# Handle case for property with both a getter and a setter
if node.node.is_property:
if not is_valid_multipart_property_def(node.node):
errors.error("Unsupported property decorator semantics", path, cdef.line)
for item in node.node.items:
prepare_method_def(ir, module_name, cdef, mapper, item)
# Handle case for regular function overload
else:
prepare_method_def(ir, module_name, cdef, mapper, get_func_def(node.node))
if any(
cls in mapper.type_to_ir and mapper.type_to_ir[cls].is_ext_class for cls in info.mro
):
errors.error(
"Non-extension classes may not inherit from extension classes", path, cdef.line)
RegisterImplInfo = Tuple[TypeInfo, FuncDef]
class SingledispatchInfo(NamedTuple):
singledispatch_impls: Dict[FuncDef, List[RegisterImplInfo]]
decorators_to_remove: Dict[FuncDef, List[int]]
def find_singledispatch_register_impls(
modules: List[MypyFile],
errors: Errors,
) -> SingledispatchInfo:
visitor = SingledispatchVisitor(errors)
for module in modules:
visitor.current_path = module.path
module.accept(visitor)
return SingledispatchInfo(visitor.singledispatch_impls, visitor.decorators_to_remove)
class SingledispatchVisitor(TraverserVisitor):
current_path: str
def __init__(self, errors: Errors) -> None:
super().__init__()
# Map of main singledispatch function to list of registered implementations
self.singledispatch_impls: DefaultDict[FuncDef, List[RegisterImplInfo]] = defaultdict(list)
# Map of decorated function to the indices of any decorators to remove
self.decorators_to_remove: Dict[FuncDef, List[int]] = {}
self.errors: Errors = errors
def visit_decorator(self, dec: Decorator) -> None:
if dec.decorators:
decorators_to_store = dec.decorators.copy()
decorators_to_remove: List[int] = []
# the index of the last non-register decorator before finding a register decorator
# when going through decorators from top to bottom
last_non_register: Optional[int] = None
for i, d in enumerate(decorators_to_store):
impl = get_singledispatch_register_call_info(d, dec.func)
if impl is not None:
self.singledispatch_impls[impl.singledispatch_func].append(
(impl.dispatch_type, dec.func))
decorators_to_remove.append(i)
if last_non_register is not None:
# found a register decorator after a non-register decorator, which we
# don't support because we'd have to make a copy of the function before
# calling the decorator so that we can call it later, which complicates
# the implementation for something that is probably not commonly used
self.errors.error(
"Calling decorator after registering function not supported",
self.current_path,
decorators_to_store[last_non_register].line,
)
else:
if refers_to_fullname(d, 'functools.singledispatch'):
decorators_to_remove.append(i)
# make sure that we still treat the function as a singledispatch function
# even if we don't find any registered implementations (which might happen
# if all registered implementations are registered dynamically)
self.singledispatch_impls.setdefault(dec.func, [])
last_non_register = i
if decorators_to_remove:
# calling register on a function that tries to dispatch based on type annotations
# raises a TypeError because compiled functions don't have an __annotations__
# attribute
self.decorators_to_remove[dec.func] = decorators_to_remove
super().visit_decorator(dec)
class RegisteredImpl(NamedTuple):
singledispatch_func: FuncDef
dispatch_type: TypeInfo
def get_singledispatch_register_call_info(decorator: Expression, func: FuncDef
) -> Optional[RegisteredImpl]:
# @fun.register(complex)
# def g(arg): ...
if (isinstance(decorator, CallExpr) and len(decorator.args) == 1
and isinstance(decorator.args[0], RefExpr)):
callee = decorator.callee
dispatch_type = decorator.args[0].node
if not isinstance(dispatch_type, TypeInfo):
return None
if isinstance(callee, MemberExpr):
return registered_impl_from_possible_register_call(callee, dispatch_type)
# @fun.register
# def g(arg: int): ...
elif isinstance(decorator, MemberExpr):
# we don't know if this is a register call yet, so we can't be sure that the function
# actually has arguments
if not func.arguments:
return None
arg_type = get_proper_type(func.arguments[0].variable.type)
if not isinstance(arg_type, Instance):
return None
info = arg_type.type
return registered_impl_from_possible_register_call(decorator, info)
return None
def registered_impl_from_possible_register_call(expr: MemberExpr, dispatch_type: TypeInfo
) -> Optional[RegisteredImpl]:
if expr.name == 'register' and isinstance(expr.expr, NameExpr):
node = expr.expr.node
if isinstance(node, Decorator):
return RegisteredImpl(node.func, dispatch_type)
return None

View file

@ -0,0 +1,540 @@
"""Special case IR generation of calls to specific builtin functions.
Most special cases should be handled using the data driven "primitive
ops" system, but certain operations require special handling that has
access to the AST/IR directly and can make decisions/optimizations
based on it. These special cases can be implemented here.
For example, we use specializers to statically emit the length of a
fixed length tuple and to emit optimized code for any()/all() calls with
generator comprehensions as the argument.
See comment below for more documentation.
"""
from typing import Callable, Optional, Dict, Tuple, List
from mypy.nodes import (
CallExpr, RefExpr, MemberExpr, NameExpr, TupleExpr, GeneratorExpr,
ListExpr, DictExpr, StrExpr, IntExpr, ARG_POS, ARG_NAMED, Expression
)
from mypy.types import AnyType, TypeOfAny
from mypyc.ir.ops import (
Value, Register, BasicBlock, Integer, RaiseStandardError, Unreachable
)
from mypyc.ir.rtypes import (
RType, RTuple, str_rprimitive, list_rprimitive, dict_rprimitive, set_rprimitive,
bool_rprimitive, c_int_rprimitive, is_dict_rprimitive
)
from mypyc.irbuild.format_str_tokenizer import (
tokenizer_format_call, join_formatted_strings, convert_format_expr_to_str, FormatOp
)
from mypyc.primitives.dict_ops import (
dict_keys_op, dict_values_op, dict_items_op, dict_setdefault_spec_init_op
)
from mypyc.primitives.list_ops import new_list_set_item_op
from mypyc.primitives.tuple_ops import new_tuple_set_item_op
from mypyc.irbuild.builder import IRBuilder
from mypyc.irbuild.for_helpers import (
translate_list_comprehension, translate_set_comprehension,
comprehension_helper, sequence_from_generator_preallocate_helper
)
# Specializers are attempted before compiling the arguments to the
# function. Specializers can return None to indicate that they failed
# and the call should be compiled normally. Otherwise they should emit
# code for the call and return a Value containing the result.
#
# Specializers take three arguments: the IRBuilder, the CallExpr being
# compiled, and the RefExpr that is the left hand side of the call.
Specializer = Callable[['IRBuilder', CallExpr, RefExpr], Optional[Value]]
# Dictionary containing all configured specializers.
#
# Specializers can operate on methods as well, and are keyed on the
# name and RType in that case.
specializers: Dict[Tuple[str, Optional[RType]], List[Specializer]] = {}
def _apply_specialization(builder: 'IRBuilder', expr: CallExpr, callee: RefExpr,
name: Optional[str], typ: Optional[RType] = None) -> Optional[Value]:
# TODO: Allow special cases to have default args or named args. Currently they don't since
# they check that everything in arg_kinds is ARG_POS.
# If there is a specializer for this function, try calling it.
# Return the first successful one.
if name and (name, typ) in specializers:
for specializer in specializers[name, typ]:
val = specializer(builder, expr, callee)
if val is not None:
return val
return None
def apply_function_specialization(builder: 'IRBuilder', expr: CallExpr,
callee: RefExpr) -> Optional[Value]:
"""Invoke the Specializer callback for a function if one has been registered"""
return _apply_specialization(builder, expr, callee, callee.fullname)
def apply_method_specialization(builder: 'IRBuilder', expr: CallExpr, callee: MemberExpr,
typ: Optional[RType] = None) -> Optional[Value]:
"""Invoke the Specializer callback for a method if one has been registered"""
name = callee.fullname if typ is None else callee.name
return _apply_specialization(builder, expr, callee, name, typ)
def specialize_function(
name: str, typ: Optional[RType] = None) -> Callable[[Specializer], Specializer]:
"""Decorator to register a function as being a specializer.
There may exist multiple specializers for one function. When
translating method calls, the earlier appended specializer has
higher priority.
"""
def wrapper(f: Specializer) -> Specializer:
specializers.setdefault((name, typ), []).append(f)
return f
return wrapper
@specialize_function('builtins.globals')
def translate_globals(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Optional[Value]:
if len(expr.args) == 0:
return builder.load_globals_dict()
return None
@specialize_function('builtins.len')
def translate_len(
builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Optional[Value]:
if (len(expr.args) == 1
and expr.arg_kinds == [ARG_POS]):
expr_rtype = builder.node_type(expr.args[0])
if isinstance(expr_rtype, RTuple):
# len() of fixed-length tuple can be trivially determined
# statically, though we still need to evaluate it.
builder.accept(expr.args[0])
return Integer(len(expr_rtype.types))
else:
obj = builder.accept(expr.args[0])
return builder.builtin_len(obj, expr.line)
return None
@specialize_function('builtins.list')
def dict_methods_fast_path(
builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Optional[Value]:
"""Specialize a common case when list() is called on a dictionary
view method call.
For example:
foo = list(bar.keys())
"""
if not (len(expr.args) == 1 and expr.arg_kinds == [ARG_POS]):
return None
arg = expr.args[0]
if not (isinstance(arg, CallExpr) and not arg.args
and isinstance(arg.callee, MemberExpr)):
return None
base = arg.callee.expr
attr = arg.callee.name
rtype = builder.node_type(base)
if not (is_dict_rprimitive(rtype) and attr in ('keys', 'values', 'items')):
return None
obj = builder.accept(base)
# Note that it is not safe to use fast methods on dict subclasses,
# so the corresponding helpers in CPy.h fallback to (inlined)
# generic logic.
if attr == 'keys':
return builder.call_c(dict_keys_op, [obj], expr.line)
elif attr == 'values':
return builder.call_c(dict_values_op, [obj], expr.line)
else:
return builder.call_c(dict_items_op, [obj], expr.line)
@specialize_function('builtins.list')
def translate_list_from_generator_call(
builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Optional[Value]:
"""Special case for simplest list comprehension.
For example:
list(f(x) for x in some_list/some_tuple/some_str)
'translate_list_comprehension()' would take care of other cases
if this fails.
"""
if (len(expr.args) == 1
and expr.arg_kinds[0] == ARG_POS
and isinstance(expr.args[0], GeneratorExpr)):
return sequence_from_generator_preallocate_helper(
builder, expr.args[0],
empty_op_llbuilder=builder.builder.new_list_op_with_length,
set_item_op=new_list_set_item_op)
return None
@specialize_function('builtins.tuple')
def translate_tuple_from_generator_call(
builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Optional[Value]:
"""Special case for simplest tuple creation from a generator.
For example:
tuple(f(x) for x in some_list/some_tuple/some_str)
'translate_safe_generator_call()' would take care of other cases
if this fails.
"""
if (len(expr.args) == 1
and expr.arg_kinds[0] == ARG_POS
and isinstance(expr.args[0], GeneratorExpr)):
return sequence_from_generator_preallocate_helper(
builder, expr.args[0],
empty_op_llbuilder=builder.builder.new_tuple_with_length,
set_item_op=new_tuple_set_item_op)
return None
@specialize_function('builtins.set')
def translate_set_from_generator_call(
builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Optional[Value]:
"""Special case for set creation from a generator.
For example:
set(f(...) for ... in iterator/nested_generators...)
"""
if (len(expr.args) == 1
and expr.arg_kinds[0] == ARG_POS
and isinstance(expr.args[0], GeneratorExpr)):
return translate_set_comprehension(builder, expr.args[0])
return None
@specialize_function('builtins.min')
@specialize_function('builtins.max')
def faster_min_max(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Optional[Value]:
if expr.arg_kinds == [ARG_POS, ARG_POS]:
x, y = builder.accept(expr.args[0]), builder.accept(expr.args[1])
result = Register(builder.node_type(expr))
# CPython evaluates arguments reversely when calling min(...) or max(...)
if callee.fullname == 'builtins.min':
comparison = builder.binary_op(y, x, '<', expr.line)
else:
comparison = builder.binary_op(y, x, '>', expr.line)
true_block, false_block, next_block = BasicBlock(), BasicBlock(), BasicBlock()
builder.add_bool_branch(comparison, true_block, false_block)
builder.activate_block(true_block)
builder.assign(result, builder.coerce(y, result.type, expr.line), expr.line)
builder.goto(next_block)
builder.activate_block(false_block)
builder.assign(result, builder.coerce(x, result.type, expr.line), expr.line)
builder.goto(next_block)
builder.activate_block(next_block)
return result
return None
@specialize_function('builtins.tuple')
@specialize_function('builtins.frozenset')
@specialize_function('builtins.dict')
@specialize_function('builtins.min')
@specialize_function('builtins.max')
@specialize_function('builtins.sorted')
@specialize_function('collections.OrderedDict')
@specialize_function('join', str_rprimitive)
@specialize_function('extend', list_rprimitive)
@specialize_function('update', dict_rprimitive)
@specialize_function('update', set_rprimitive)
def translate_safe_generator_call(
builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Optional[Value]:
"""Special cases for things that consume iterators where we know we
can safely compile a generator into a list.
"""
if (len(expr.args) > 0
and expr.arg_kinds[0] == ARG_POS
and isinstance(expr.args[0], GeneratorExpr)):
if isinstance(callee, MemberExpr):
return builder.gen_method_call(
builder.accept(callee.expr), callee.name,
([translate_list_comprehension(builder, expr.args[0])]
+ [builder.accept(arg) for arg in expr.args[1:]]),
builder.node_type(expr), expr.line, expr.arg_kinds, expr.arg_names)
else:
return builder.call_refexpr_with_args(
expr, callee,
([translate_list_comprehension(builder, expr.args[0])]
+ [builder.accept(arg) for arg in expr.args[1:]]))
return None
@specialize_function('builtins.any')
def translate_any_call(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Optional[Value]:
if (len(expr.args) == 1
and expr.arg_kinds == [ARG_POS]
and isinstance(expr.args[0], GeneratorExpr)):
return any_all_helper(builder, expr.args[0], builder.false, lambda x: x, builder.true)
return None
@specialize_function('builtins.all')
def translate_all_call(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Optional[Value]:
if (len(expr.args) == 1
and expr.arg_kinds == [ARG_POS]
and isinstance(expr.args[0], GeneratorExpr)):
return any_all_helper(
builder, expr.args[0],
builder.true,
lambda x: builder.unary_op(x, 'not', expr.line),
builder.false
)
return None
def any_all_helper(builder: IRBuilder,
gen: GeneratorExpr,
initial_value: Callable[[], Value],
modify: Callable[[Value], Value],
new_value: Callable[[], Value]) -> Value:
retval = Register(bool_rprimitive)
builder.assign(retval, initial_value(), -1)
loop_params = list(zip(gen.indices, gen.sequences, gen.condlists))
true_block, false_block, exit_block = BasicBlock(), BasicBlock(), BasicBlock()
def gen_inner_stmts() -> None:
comparison = modify(builder.accept(gen.left_expr))
builder.add_bool_branch(comparison, true_block, false_block)
builder.activate_block(true_block)
builder.assign(retval, new_value(), -1)
builder.goto(exit_block)
builder.activate_block(false_block)
comprehension_helper(builder, loop_params, gen_inner_stmts, gen.line)
builder.goto_and_activate(exit_block)
return retval
@specialize_function('builtins.sum')
def translate_sum_call(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Optional[Value]:
# specialized implementation is used if:
# - only one or two arguments given (if not, sum() has been given invalid arguments)
# - first argument is a Generator (there is no benefit to optimizing the performance of eg.
# sum([1, 2, 3]), so non-Generator Iterables are not handled)
if not (len(expr.args) in (1, 2)
and expr.arg_kinds[0] == ARG_POS
and isinstance(expr.args[0], GeneratorExpr)):
return None
# handle 'start' argument, if given
if len(expr.args) == 2:
# ensure call to sum() was properly constructed
if not expr.arg_kinds[1] in (ARG_POS, ARG_NAMED):
return None
start_expr = expr.args[1]
else:
start_expr = IntExpr(0)
gen_expr = expr.args[0]
target_type = builder.node_type(expr)
retval = Register(target_type)
builder.assign(retval, builder.coerce(builder.accept(start_expr), target_type, -1), -1)
def gen_inner_stmts() -> None:
call_expr = builder.accept(gen_expr.left_expr)
builder.assign(retval, builder.binary_op(retval, call_expr, '+', -1), -1)
loop_params = list(zip(gen_expr.indices, gen_expr.sequences, gen_expr.condlists))
comprehension_helper(builder, loop_params, gen_inner_stmts, gen_expr.line)
return retval
@specialize_function('dataclasses.field')
@specialize_function('attr.ib')
@specialize_function('attr.attrib')
@specialize_function('attr.Factory')
def translate_dataclasses_field_call(
builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Optional[Value]:
"""Special case for 'dataclasses.field', 'attr.attrib', and 'attr.Factory'
function calls because the results of such calls are type-checked
by mypy using the types of the arguments to their respective
functions, resulting in attempted coercions by mypyc that throw a
runtime error.
"""
builder.types[expr] = AnyType(TypeOfAny.from_error)
return None
@specialize_function('builtins.next')
def translate_next_call(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Optional[Value]:
"""Special case for calling next() on a generator expression, an
idiom that shows up some in mypy.
For example, next(x for x in l if x.id == 12, None) will
generate code that searches l for an element where x.id == 12
and produce the first such object, or None if no such element
exists.
"""
if not (expr.arg_kinds in ([ARG_POS], [ARG_POS, ARG_POS])
and isinstance(expr.args[0], GeneratorExpr)):
return None
gen = expr.args[0]
retval = Register(builder.node_type(expr))
default_val = builder.accept(expr.args[1]) if len(expr.args) > 1 else None
exit_block = BasicBlock()
def gen_inner_stmts() -> None:
# next takes the first element of the generator, so if
# something gets produced, we are done.
builder.assign(retval, builder.accept(gen.left_expr), gen.left_expr.line)
builder.goto(exit_block)
loop_params = list(zip(gen.indices, gen.sequences, gen.condlists))
comprehension_helper(builder, loop_params, gen_inner_stmts, gen.line)
# Now we need the case for when nothing got hit. If there was
# a default value, we produce it, and otherwise we raise
# StopIteration.
if default_val:
builder.assign(retval, default_val, gen.left_expr.line)
builder.goto(exit_block)
else:
builder.add(RaiseStandardError(RaiseStandardError.STOP_ITERATION, None, expr.line))
builder.add(Unreachable())
builder.activate_block(exit_block)
return retval
@specialize_function('builtins.isinstance')
def translate_isinstance(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Optional[Value]:
"""Special case for builtins.isinstance.
Prevent coercions on the thing we are checking the instance of -
there is no need to coerce something to a new type before checking
what type it is, and the coercion could lead to bugs.
"""
if (len(expr.args) == 2
and expr.arg_kinds == [ARG_POS, ARG_POS]
and isinstance(expr.args[1], (RefExpr, TupleExpr))):
builder.types[expr.args[0]] = AnyType(TypeOfAny.from_error)
irs = builder.flatten_classes(expr.args[1])
if irs is not None:
return builder.builder.isinstance_helper(builder.accept(expr.args[0]), irs, expr.line)
return None
@specialize_function('setdefault', dict_rprimitive)
def translate_dict_setdefault(
builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Optional[Value]:
"""Special case for 'dict.setdefault' which would only construct
default empty collection when needed.
The dict_setdefault_spec_init_op checks whether the dict contains
the key and would construct the empty collection only once.
For example, this specializer works for the following cases:
d.setdefault(key, set()).add(value)
d.setdefault(key, []).append(value)
d.setdefault(key, {})[inner_key] = inner_val
"""
if (len(expr.args) == 2
and expr.arg_kinds == [ARG_POS, ARG_POS]
and isinstance(callee, MemberExpr)):
arg = expr.args[1]
if isinstance(arg, ListExpr):
if len(arg.items):
return None
data_type = Integer(1, c_int_rprimitive, expr.line)
elif isinstance(arg, DictExpr):
if len(arg.items):
return None
data_type = Integer(2, c_int_rprimitive, expr.line)
elif (isinstance(arg, CallExpr) and isinstance(arg.callee, NameExpr)
and arg.callee.fullname == 'builtins.set'):
if len(arg.args):
return None
data_type = Integer(3, c_int_rprimitive, expr.line)
else:
return None
callee_dict = builder.accept(callee.expr)
key_val = builder.accept(expr.args[0])
return builder.call_c(dict_setdefault_spec_init_op,
[callee_dict, key_val, data_type],
expr.line)
return None
@specialize_function('format', str_rprimitive)
def translate_str_format(
builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Optional[Value]:
if (isinstance(callee, MemberExpr) and isinstance(callee.expr, StrExpr)
and expr.arg_kinds.count(ARG_POS) == len(expr.arg_kinds)):
format_str = callee.expr.value
tokens = tokenizer_format_call(format_str)
if tokens is None:
return None
literals, format_ops = tokens
# Convert variables to strings
substitutions = convert_format_expr_to_str(builder, format_ops, expr.args, expr.line)
if substitutions is None:
return None
return join_formatted_strings(builder, literals, substitutions, expr.line)
return None
@specialize_function('join', str_rprimitive)
def translate_fstring(
builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Optional[Value]:
"""Special case for f-string, which is translated into str.join()
in mypy AST.
This specializer optimizes simplest f-strings which don't contain
any format operation.
"""
if (isinstance(callee, MemberExpr)
and isinstance(callee.expr, StrExpr) and callee.expr.value == ''
and expr.arg_kinds == [ARG_POS] and isinstance(expr.args[0], ListExpr)):
for item in expr.args[0].items:
if isinstance(item, StrExpr):
continue
elif isinstance(item, CallExpr):
if (not isinstance(item.callee, MemberExpr)
or item.callee.name != 'format'):
return None
elif (not isinstance(item.callee.expr, StrExpr)
or item.callee.expr.value != '{:{}}'):
return None
if not isinstance(item.args[1], StrExpr) or item.args[1].value != '':
return None
else:
return None
format_ops = []
exprs: List[Expression] = []
for item in expr.args[0].items:
if isinstance(item, StrExpr) and item.value != '':
format_ops.append(FormatOp.STR)
exprs.append(item)
elif isinstance(item, CallExpr):
format_ops.append(FormatOp.STR)
exprs.append(item.args[0])
substitutions = convert_format_expr_to_str(builder, format_ops, exprs, expr.line)
if substitutions is None:
return None
return join_formatted_strings(builder, None, substitutions, expr.line)
return None

View file

@ -0,0 +1,682 @@
"""Transform mypy statement ASTs to mypyc IR (Intermediate Representation).
The top-level AST transformation logic is implemented in mypyc.irbuild.visitor
and mypyc.irbuild.builder.
A few statements are transformed in mypyc.irbuild.function (yield, for example).
"""
from typing import Optional, List, Tuple, Sequence, Callable
import importlib.util
from mypy.nodes import (
Block, ExpressionStmt, ReturnStmt, AssignmentStmt, OperatorAssignmentStmt, IfStmt, WhileStmt,
ForStmt, BreakStmt, ContinueStmt, RaiseStmt, TryStmt, WithStmt, AssertStmt, DelStmt,
Expression, StrExpr, TempNode, Lvalue, Import, ImportFrom, ImportAll, TupleExpr, ListExpr,
StarExpr
)
from mypyc.ir.ops import (
Assign, Unreachable, RaiseStandardError, LoadErrorValue, BasicBlock, TupleGet, Value, Register,
Branch, NO_TRACEBACK_LINE_NO
)
from mypyc.ir.rtypes import RInstance, exc_rtuple
from mypyc.primitives.generic_ops import py_delattr_op
from mypyc.primitives.misc_ops import type_op, import_from_op
from mypyc.primitives.exc_ops import (
raise_exception_op, reraise_exception_op, error_catch_op, exc_matches_op, restore_exc_info_op,
get_exc_value_op, keep_propagating_op, get_exc_info_op
)
from mypyc.irbuild.targets import (
AssignmentTarget, AssignmentTargetRegister, AssignmentTargetIndex, AssignmentTargetAttr,
AssignmentTargetTuple
)
from mypyc.irbuild.nonlocalcontrol import (
ExceptNonlocalControl, FinallyNonlocalControl, TryFinallyNonlocalControl
)
from mypyc.irbuild.for_helpers import for_loop_helper
from mypyc.irbuild.builder import IRBuilder
GenFunc = Callable[[], None]
def transform_block(builder: IRBuilder, block: Block) -> None:
if not block.is_unreachable:
for stmt in block.body:
builder.accept(stmt)
# Raise a RuntimeError if we hit a non-empty unreachable block.
# Don't complain about empty unreachable blocks, since mypy inserts
# those after `if MYPY`.
elif block.body:
builder.add(RaiseStandardError(RaiseStandardError.RUNTIME_ERROR,
'Reached allegedly unreachable code!',
block.line))
builder.add(Unreachable())
def transform_expression_stmt(builder: IRBuilder, stmt: ExpressionStmt) -> None:
if isinstance(stmt.expr, StrExpr):
# Docstring. Ignore
return
# ExpressionStmts do not need to be coerced like other Expressions.
stmt.expr.accept(builder.visitor)
def transform_return_stmt(builder: IRBuilder, stmt: ReturnStmt) -> None:
if stmt.expr:
retval = builder.accept(stmt.expr)
else:
retval = builder.builder.none()
retval = builder.coerce(retval, builder.ret_types[-1], stmt.line)
builder.nonlocal_control[-1].gen_return(builder, retval, stmt.line)
def transform_assignment_stmt(builder: IRBuilder, stmt: AssignmentStmt) -> None:
lvalues = stmt.lvalues
assert len(lvalues) >= 1
builder.disallow_class_assignments(lvalues, stmt.line)
first_lvalue = lvalues[0]
if stmt.type and isinstance(stmt.rvalue, TempNode):
# This is actually a variable annotation without initializer. Don't generate
# an assignment but we need to call get_assignment_target since it adds a
# name binding as a side effect.
builder.get_assignment_target(first_lvalue, stmt.line)
return
# Special case multiple assignments like 'x, y = e1, e2'.
if (isinstance(first_lvalue, (TupleExpr, ListExpr))
and isinstance(stmt.rvalue, (TupleExpr, ListExpr))
and len(first_lvalue.items) == len(stmt.rvalue.items)
and all(is_simple_lvalue(item) for item in first_lvalue.items)
and len(lvalues) == 1):
temps = []
for right in stmt.rvalue.items:
rvalue_reg = builder.accept(right)
temp = Register(rvalue_reg.type)
builder.assign(temp, rvalue_reg, stmt.line)
temps.append(temp)
for (left, temp) in zip(first_lvalue.items, temps):
assignment_target = builder.get_assignment_target(left)
builder.assign(assignment_target, temp, stmt.line)
return
line = stmt.rvalue.line
rvalue_reg = builder.accept(stmt.rvalue)
if builder.non_function_scope() and stmt.is_final_def:
builder.init_final_static(first_lvalue, rvalue_reg)
for lvalue in lvalues:
target = builder.get_assignment_target(lvalue)
builder.assign(target, rvalue_reg, line)
def is_simple_lvalue(expr: Expression) -> bool:
return not isinstance(expr, (StarExpr, ListExpr, TupleExpr))
def transform_operator_assignment_stmt(builder: IRBuilder, stmt: OperatorAssignmentStmt) -> None:
"""Operator assignment statement such as x += 1"""
builder.disallow_class_assignments([stmt.lvalue], stmt.line)
target = builder.get_assignment_target(stmt.lvalue)
target_value = builder.read(target, stmt.line)
rreg = builder.accept(stmt.rvalue)
# the Python parser strips the '=' from operator assignment statements, so re-add it
op = stmt.op + '='
res = builder.binary_op(target_value, rreg, op, stmt.line)
# usually operator assignments are done in-place
# but when target doesn't support that we need to manually assign
builder.assign(target, res, res.line)
def transform_import(builder: IRBuilder, node: Import) -> None:
if node.is_mypy_only:
return
globals = builder.load_globals_dict()
for node_id, as_name in node.ids:
builder.gen_import(node_id, node.line)
# Update the globals dict with the appropriate module:
# * For 'import foo.bar as baz' we add 'foo.bar' with the name 'baz'
# * For 'import foo.bar' we add 'foo' with the name 'foo'
# Typically we then ignore these entries and access things directly
# via the module static, but we will use the globals version for modules
# that mypy couldn't find, since it doesn't analyze module references
# from those properly.
# TODO: Don't add local imports to the global namespace
# Miscompiling imports inside of functions, like below in import from.
if as_name:
name = as_name
base = node_id
else:
base = name = node_id.split('.')[0]
obj = builder.get_module(base, node.line)
builder.gen_method_call(
globals, '__setitem__', [builder.load_str(name), obj],
result_type=None, line=node.line)
def transform_import_from(builder: IRBuilder, node: ImportFrom) -> None:
if node.is_mypy_only:
return
module_state = builder.graph[builder.module_name]
if module_state.ancestors is not None and module_state.ancestors:
module_package = module_state.ancestors[0]
elif builder.module_path.endswith("__init__.py"):
module_package = builder.module_name
else:
module_package = ''
id = importlib.util.resolve_name('.' * node.relative + node.id, module_package)
globals = builder.load_globals_dict()
imported_names = [name for name, _ in node.names]
module = builder.gen_import_from(id, globals, imported_names, node.line)
# Copy everything into our module's dict.
# Note that we miscompile import from inside of functions here,
# since that case *shouldn't* load it into the globals dict.
# This probably doesn't matter much and the code runs basically right.
for name, maybe_as_name in node.names:
as_name = maybe_as_name or name
obj = builder.call_c(import_from_op,
[module, builder.load_str(id),
builder.load_str(name), builder.load_str(as_name)],
node.line)
builder.gen_method_call(
globals, '__setitem__', [builder.load_str(as_name), obj],
result_type=None, line=node.line)
def transform_import_all(builder: IRBuilder, node: ImportAll) -> None:
if node.is_mypy_only:
return
builder.gen_import(node.id, node.line)
def transform_if_stmt(builder: IRBuilder, stmt: IfStmt) -> None:
if_body, next = BasicBlock(), BasicBlock()
else_body = BasicBlock() if stmt.else_body else next
# If statements are normalized
assert len(stmt.expr) == 1
builder.process_conditional(stmt.expr[0], if_body, else_body)
builder.activate_block(if_body)
builder.accept(stmt.body[0])
builder.goto(next)
if stmt.else_body:
builder.activate_block(else_body)
builder.accept(stmt.else_body)
builder.goto(next)
builder.activate_block(next)
def transform_while_stmt(builder: IRBuilder, s: WhileStmt) -> None:
body, next, top, else_block = BasicBlock(), BasicBlock(), BasicBlock(), BasicBlock()
normal_loop_exit = else_block if s.else_body is not None else next
builder.push_loop_stack(top, next)
# Split block so that we get a handle to the top of the loop.
builder.goto_and_activate(top)
builder.process_conditional(s.expr, body, normal_loop_exit)
builder.activate_block(body)
builder.accept(s.body)
# Add branch to the top at the end of the body.
builder.goto(top)
builder.pop_loop_stack()
if s.else_body is not None:
builder.activate_block(else_block)
builder.accept(s.else_body)
builder.goto(next)
builder.activate_block(next)
def transform_for_stmt(builder: IRBuilder, s: ForStmt) -> None:
if s.is_async:
builder.error('async for is unimplemented', s.line)
def body() -> None:
builder.accept(s.body)
def else_block() -> None:
assert s.else_body is not None
builder.accept(s.else_body)
for_loop_helper(builder, s.index, s.expr, body,
else_block if s.else_body else None, s.line)
def transform_break_stmt(builder: IRBuilder, node: BreakStmt) -> None:
builder.nonlocal_control[-1].gen_break(builder, node.line)
def transform_continue_stmt(builder: IRBuilder, node: ContinueStmt) -> None:
builder.nonlocal_control[-1].gen_continue(builder, node.line)
def transform_raise_stmt(builder: IRBuilder, s: RaiseStmt) -> None:
if s.expr is None:
builder.call_c(reraise_exception_op, [], NO_TRACEBACK_LINE_NO)
builder.add(Unreachable())
return
exc = builder.accept(s.expr)
builder.call_c(raise_exception_op, [exc], s.line)
builder.add(Unreachable())
def transform_try_except(builder: IRBuilder,
body: GenFunc,
handlers: Sequence[
Tuple[Optional[Expression], Optional[Expression], GenFunc]],
else_body: Optional[GenFunc],
line: int) -> None:
"""Generalized try/except/else handling that takes functions to gen the bodies.
The point of this is to also be able to support with."""
assert handlers, "try needs except"
except_entry, exit_block, cleanup_block = BasicBlock(), BasicBlock(), BasicBlock()
double_except_block = BasicBlock()
# If there is an else block, jump there after the try, otherwise just leave
else_block = BasicBlock() if else_body else exit_block
# Compile the try block with an error handler
builder.builder.push_error_handler(except_entry)
builder.goto_and_activate(BasicBlock())
body()
builder.goto(else_block)
builder.builder.pop_error_handler()
# The error handler catches the error and then checks it
# against the except clauses. We compile the error handler
# itself with an error handler so that it can properly restore
# the *old* exc_info if an exception occurs.
# The exception chaining will be done automatically when the
# exception is raised, based on the exception in exc_info.
builder.builder.push_error_handler(double_except_block)
builder.activate_block(except_entry)
old_exc = builder.maybe_spill(builder.call_c(error_catch_op, [], line))
# Compile the except blocks with the nonlocal control flow overridden to clear exc_info
builder.nonlocal_control.append(
ExceptNonlocalControl(builder.nonlocal_control[-1], old_exc))
# Process the bodies
for type, var, handler_body in handlers:
next_block = None
if type:
next_block, body_block = BasicBlock(), BasicBlock()
matches = builder.call_c(
exc_matches_op, [builder.accept(type)], type.line
)
builder.add(Branch(matches, body_block, next_block, Branch.BOOL))
builder.activate_block(body_block)
if var:
target = builder.get_assignment_target(var)
builder.assign(
target,
builder.call_c(get_exc_value_op, [], var.line),
var.line
)
handler_body()
builder.goto(cleanup_block)
if next_block:
builder.activate_block(next_block)
# Reraise the exception if needed
if next_block:
builder.call_c(reraise_exception_op, [], NO_TRACEBACK_LINE_NO)
builder.add(Unreachable())
builder.nonlocal_control.pop()
builder.builder.pop_error_handler()
# Cleanup for if we leave except through normal control flow:
# restore the saved exc_info information and continue propagating
# the exception if it exists.
builder.activate_block(cleanup_block)
builder.call_c(restore_exc_info_op, [builder.read(old_exc)], line)
builder.goto(exit_block)
# Cleanup for if we leave except through a raised exception:
# restore the saved exc_info information and continue propagating
# the exception.
builder.activate_block(double_except_block)
builder.call_c(restore_exc_info_op, [builder.read(old_exc)], line)
builder.call_c(keep_propagating_op, [], NO_TRACEBACK_LINE_NO)
builder.add(Unreachable())
# If present, compile the else body in the obvious way
if else_body:
builder.activate_block(else_block)
else_body()
builder.goto(exit_block)
builder.activate_block(exit_block)
def transform_try_except_stmt(builder: IRBuilder, t: TryStmt) -> None:
def body() -> None:
builder.accept(t.body)
# Work around scoping woes
def make_handler(body: Block) -> GenFunc:
return lambda: builder.accept(body)
handlers = [(type, var, make_handler(body))
for type, var, body in zip(t.types, t.vars, t.handlers)]
else_body = (lambda: builder.accept(t.else_body)) if t.else_body else None
transform_try_except(builder, body, handlers, else_body, t.line)
def try_finally_try(builder: IRBuilder,
err_handler: BasicBlock,
return_entry: BasicBlock,
main_entry: BasicBlock,
try_body: GenFunc) -> Optional[Register]:
# Compile the try block with an error handler
control = TryFinallyNonlocalControl(return_entry)
builder.builder.push_error_handler(err_handler)
builder.nonlocal_control.append(control)
builder.goto_and_activate(BasicBlock())
try_body()
builder.goto(main_entry)
builder.nonlocal_control.pop()
builder.builder.pop_error_handler()
return control.ret_reg
def try_finally_entry_blocks(builder: IRBuilder,
err_handler: BasicBlock,
return_entry: BasicBlock,
main_entry: BasicBlock,
finally_block: BasicBlock,
ret_reg: Optional[Register]) -> Value:
old_exc = Register(exc_rtuple)
# Entry block for non-exceptional flow
builder.activate_block(main_entry)
if ret_reg:
builder.add(
Assign(
ret_reg,
builder.add(LoadErrorValue(builder.ret_types[-1]))
)
)
builder.goto(return_entry)
builder.activate_block(return_entry)
builder.add(Assign(old_exc, builder.add(LoadErrorValue(exc_rtuple))))
builder.goto(finally_block)
# Entry block for errors
builder.activate_block(err_handler)
if ret_reg:
builder.add(
Assign(
ret_reg,
builder.add(LoadErrorValue(builder.ret_types[-1]))
)
)
builder.add(Assign(old_exc, builder.call_c(error_catch_op, [], -1)))
builder.goto(finally_block)
return old_exc
def try_finally_body(
builder: IRBuilder,
finally_block: BasicBlock,
finally_body: GenFunc,
ret_reg: Optional[Value],
old_exc: Value) -> Tuple[BasicBlock, FinallyNonlocalControl]:
cleanup_block = BasicBlock()
# Compile the finally block with the nonlocal control flow overridden to restore exc_info
builder.builder.push_error_handler(cleanup_block)
finally_control = FinallyNonlocalControl(
builder.nonlocal_control[-1], ret_reg, old_exc)
builder.nonlocal_control.append(finally_control)
builder.activate_block(finally_block)
finally_body()
builder.nonlocal_control.pop()
return cleanup_block, finally_control
def try_finally_resolve_control(builder: IRBuilder,
cleanup_block: BasicBlock,
finally_control: FinallyNonlocalControl,
old_exc: Value,
ret_reg: Optional[Value]) -> BasicBlock:
"""Resolve the control flow out of a finally block.
This means returning if there was a return, propagating
exceptions, break/continue (soon), or just continuing on.
"""
reraise, rest = BasicBlock(), BasicBlock()
builder.add(Branch(old_exc, rest, reraise, Branch.IS_ERROR))
# Reraise the exception if there was one
builder.activate_block(reraise)
builder.call_c(reraise_exception_op, [], NO_TRACEBACK_LINE_NO)
builder.add(Unreachable())
builder.builder.pop_error_handler()
# If there was a return, keep returning
if ret_reg:
builder.activate_block(rest)
return_block, rest = BasicBlock(), BasicBlock()
builder.add(Branch(ret_reg, rest, return_block, Branch.IS_ERROR))
builder.activate_block(return_block)
builder.nonlocal_control[-1].gen_return(builder, ret_reg, -1)
# TODO: handle break/continue
builder.activate_block(rest)
out_block = BasicBlock()
builder.goto(out_block)
# If there was an exception, restore again
builder.activate_block(cleanup_block)
finally_control.gen_cleanup(builder, -1)
builder.call_c(keep_propagating_op, [], NO_TRACEBACK_LINE_NO)
builder.add(Unreachable())
return out_block
def transform_try_finally_stmt(builder: IRBuilder,
try_body: GenFunc,
finally_body: GenFunc) -> None:
"""Generalized try/finally handling that takes functions to gen the bodies.
The point of this is to also be able to support with."""
# Finally is a big pain, because there are so many ways that
# exits can occur. We emit 10+ basic blocks for every finally!
err_handler, main_entry, return_entry, finally_block = (
BasicBlock(), BasicBlock(), BasicBlock(), BasicBlock())
# Compile the body of the try
ret_reg = try_finally_try(
builder, err_handler, return_entry, main_entry, try_body)
# Set up the entry blocks for the finally statement
old_exc = try_finally_entry_blocks(
builder, err_handler, return_entry, main_entry, finally_block, ret_reg)
# Compile the body of the finally
cleanup_block, finally_control = try_finally_body(
builder, finally_block, finally_body, ret_reg, old_exc)
# Resolve the control flow out of the finally block
out_block = try_finally_resolve_control(
builder, cleanup_block, finally_control, old_exc, ret_reg)
builder.activate_block(out_block)
def transform_try_stmt(builder: IRBuilder, t: TryStmt) -> None:
# Our compilation strategy for try/except/else/finally is to
# treat try/except/else and try/finally as separate language
# constructs that we compile separately. When we have a
# try/except/else/finally, we treat the try/except/else as the
# body of a try/finally block.
if t.finally_body:
def transform_try_body() -> None:
if t.handlers:
transform_try_except_stmt(builder, t)
else:
builder.accept(t.body)
body = t.finally_body
transform_try_finally_stmt(builder, transform_try_body, lambda: builder.accept(body))
else:
transform_try_except_stmt(builder, t)
def get_sys_exc_info(builder: IRBuilder) -> List[Value]:
exc_info = builder.call_c(get_exc_info_op, [], -1)
return [builder.add(TupleGet(exc_info, i, -1)) for i in range(3)]
def transform_with(builder: IRBuilder,
expr: Expression,
target: Optional[Lvalue],
body: GenFunc,
line: int) -> None:
# This is basically a straight transcription of the Python code in PEP 343.
# I don't actually understand why a bunch of it is the way it is.
# We could probably optimize the case where the manager is compiled by us,
# but that is not our common case at all, so.
mgr_v = builder.accept(expr)
typ = builder.call_c(type_op, [mgr_v], line)
exit_ = builder.maybe_spill(builder.py_get_attr(typ, '__exit__', line))
value = builder.py_call(
builder.py_get_attr(typ, '__enter__', line), [mgr_v], line
)
mgr = builder.maybe_spill(mgr_v)
exc = builder.maybe_spill_assignable(builder.true())
def try_body() -> None:
if target:
builder.assign(builder.get_assignment_target(target), value, line)
body()
def except_body() -> None:
builder.assign(exc, builder.false(), line)
out_block, reraise_block = BasicBlock(), BasicBlock()
builder.add_bool_branch(
builder.py_call(builder.read(exit_),
[builder.read(mgr)] + get_sys_exc_info(builder), line),
out_block,
reraise_block
)
builder.activate_block(reraise_block)
builder.call_c(reraise_exception_op, [], NO_TRACEBACK_LINE_NO)
builder.add(Unreachable())
builder.activate_block(out_block)
def finally_body() -> None:
out_block, exit_block = BasicBlock(), BasicBlock()
builder.add(
Branch(builder.read(exc), exit_block, out_block, Branch.BOOL)
)
builder.activate_block(exit_block)
none = builder.none_object()
builder.py_call(
builder.read(exit_), [builder.read(mgr), none, none, none], line
)
builder.goto_and_activate(out_block)
transform_try_finally_stmt(
builder,
lambda: transform_try_except(builder,
try_body,
[(None, None, except_body)],
None,
line),
finally_body
)
def transform_with_stmt(builder: IRBuilder, o: WithStmt) -> None:
if o.is_async:
builder.error('async with is unimplemented', o.line)
# Generate separate logic for each expr in it, left to right
def generate(i: int) -> None:
if i >= len(o.expr):
builder.accept(o.body)
else:
transform_with(builder, o.expr[i], o.target[i], lambda: generate(i + 1), o.line)
generate(0)
def transform_assert_stmt(builder: IRBuilder, a: AssertStmt) -> None:
if builder.options.strip_asserts:
return
cond = builder.accept(a.expr)
ok_block, error_block = BasicBlock(), BasicBlock()
builder.add_bool_branch(cond, ok_block, error_block)
builder.activate_block(error_block)
if a.msg is None:
# Special case (for simpler generated code)
builder.add(RaiseStandardError(RaiseStandardError.ASSERTION_ERROR, None, a.line))
elif isinstance(a.msg, StrExpr):
# Another special case
builder.add(RaiseStandardError(RaiseStandardError.ASSERTION_ERROR, a.msg.value,
a.line))
else:
# The general case -- explicitly construct an exception instance
message = builder.accept(a.msg)
exc_type = builder.load_module_attr_by_fullname('builtins.AssertionError', a.line)
exc = builder.py_call(exc_type, [message], a.line)
builder.call_c(raise_exception_op, [exc], a.line)
builder.add(Unreachable())
builder.activate_block(ok_block)
def transform_del_stmt(builder: IRBuilder, o: DelStmt) -> None:
transform_del_item(builder, builder.get_assignment_target(o.expr), o.line)
def transform_del_item(builder: IRBuilder, target: AssignmentTarget, line: int) -> None:
if isinstance(target, AssignmentTargetIndex):
builder.gen_method_call(
target.base,
'__delitem__',
[target.index],
result_type=None,
line=line
)
elif isinstance(target, AssignmentTargetAttr):
if isinstance(target.obj_type, RInstance):
cl = target.obj_type.class_ir
if not cl.is_deletable(target.attr):
builder.error('"{}" cannot be deleted'.format(target.attr), line)
builder.note(
'Using "__deletable__ = ' +
'[\'<attr>\']" in the class body enables "del obj.<attr>"', line)
key = builder.load_str(target.attr)
builder.call_c(py_delattr_op, [target.obj, key], line)
elif isinstance(target, AssignmentTargetRegister):
# Delete a local by assigning an error value to it, which will
# prompt the insertion of uninit checks.
builder.add(Assign(target.register,
builder.add(LoadErrorValue(target.type, undefines=True))))
elif isinstance(target, AssignmentTargetTuple):
for subtarget in target.items:
transform_del_item(builder, subtarget, line)

View file

@ -0,0 +1,58 @@
from typing import List, Optional
from mypyc.ir.ops import Value, Register
from mypyc.ir.rtypes import RType, RInstance, object_rprimitive
class AssignmentTarget:
"""Abstract base class for assignment targets during IR building."""
type: RType = object_rprimitive
class AssignmentTargetRegister(AssignmentTarget):
"""Register as an assignment target.
This is used for local variables and some temporaries.
"""
def __init__(self, register: Register) -> None:
self.register = register
self.type = register.type
class AssignmentTargetIndex(AssignmentTarget):
"""base[index] as assignment target"""
def __init__(self, base: Value, index: Value) -> None:
self.base = base
self.index = index
# TODO: object_rprimitive won't be right for user-defined classes. Store the
# lvalue type in mypy and use a better type to avoid unneeded boxing.
self.type = object_rprimitive
class AssignmentTargetAttr(AssignmentTarget):
"""obj.attr as assignment target"""
def __init__(self, obj: Value, attr: str) -> None:
self.obj = obj
self.attr = attr
if isinstance(obj.type, RInstance) and obj.type.class_ir.has_attr(attr):
# Native attribute reference
self.obj_type: RType = obj.type
self.type = obj.type.attr_type(attr)
else:
# Python attribute reference
self.obj_type = object_rprimitive
self.type = object_rprimitive
class AssignmentTargetTuple(AssignmentTarget):
"""x, ..., y as assignment target"""
def __init__(self,
items: List[AssignmentTarget],
star_idx: Optional[int] = None) -> None:
self.items = items
self.star_idx = star_idx

View file

@ -0,0 +1,156 @@
"""Various utilities that don't depend on other modules in mypyc.irbuild."""
from typing import Dict, Any, Union, Optional
from mypy.nodes import (
ClassDef, FuncDef, Decorator, OverloadedFuncDef, StrExpr, CallExpr, RefExpr, Expression,
IntExpr, FloatExpr, Var, NameExpr, TupleExpr, UnaryExpr, BytesExpr,
ArgKind, ARG_NAMED, ARG_NAMED_OPT, ARG_POS, ARG_OPT, GDEF,
)
DATACLASS_DECORATORS = {
'dataclasses.dataclass',
'attr.s',
'attr.attrs',
}
def is_trait_decorator(d: Expression) -> bool:
return isinstance(d, RefExpr) and d.fullname == 'mypy_extensions.trait'
def is_trait(cdef: ClassDef) -> bool:
return any(is_trait_decorator(d) for d in cdef.decorators) or cdef.info.is_protocol
def dataclass_decorator_type(d: Expression) -> Optional[str]:
if isinstance(d, RefExpr) and d.fullname in DATACLASS_DECORATORS:
return d.fullname.split('.')[0]
elif (isinstance(d, CallExpr)
and isinstance(d.callee, RefExpr)
and d.callee.fullname in DATACLASS_DECORATORS):
name = d.callee.fullname.split('.')[0]
if name == 'attr' and 'auto_attribs' in d.arg_names:
# Note: the mypy attrs plugin checks that the value of auto_attribs is
# not computed at runtime, so we don't need to perform that check here
auto = d.args[d.arg_names.index('auto_attribs')]
if isinstance(auto, NameExpr) and auto.name == 'True':
return 'attr-auto'
return name
else:
return None
def is_dataclass_decorator(d: Expression) -> bool:
return dataclass_decorator_type(d) is not None
def is_dataclass(cdef: ClassDef) -> bool:
return any(is_dataclass_decorator(d) for d in cdef.decorators)
def dataclass_type(cdef: ClassDef) -> Optional[str]:
for d in cdef.decorators:
typ = dataclass_decorator_type(d)
if typ is not None:
return typ
return None
def get_mypyc_attr_literal(e: Expression) -> Any:
"""Convert an expression from a mypyc_attr decorator to a value.
Supports a pretty limited range."""
if isinstance(e, (StrExpr, IntExpr, FloatExpr)):
return e.value
elif isinstance(e, RefExpr) and e.fullname == 'builtins.True':
return True
elif isinstance(e, RefExpr) and e.fullname == 'builtins.False':
return False
elif isinstance(e, RefExpr) and e.fullname == 'builtins.None':
return None
return NotImplemented
def get_mypyc_attr_call(d: Expression) -> Optional[CallExpr]:
"""Check if an expression is a call to mypyc_attr and return it if so."""
if (
isinstance(d, CallExpr)
and isinstance(d.callee, RefExpr)
and d.callee.fullname == 'mypy_extensions.mypyc_attr'
):
return d
return None
def get_mypyc_attrs(stmt: Union[ClassDef, Decorator]) -> Dict[str, Any]:
"""Collect all the mypyc_attr attributes on a class definition or a function."""
attrs: Dict[str, Any] = {}
for dec in stmt.decorators:
d = get_mypyc_attr_call(dec)
if d:
for name, arg in zip(d.arg_names, d.args):
if name is None:
if isinstance(arg, StrExpr):
attrs[arg.value] = True
else:
attrs[name] = get_mypyc_attr_literal(arg)
return attrs
def is_extension_class(cdef: ClassDef) -> bool:
if any(
not is_trait_decorator(d)
and not is_dataclass_decorator(d)
and not get_mypyc_attr_call(d)
for d in cdef.decorators
):
return False
if cdef.info.typeddict_type:
return False
if cdef.info.is_named_tuple:
return False
if (cdef.info.metaclass_type and cdef.info.metaclass_type.type.fullname not in (
'abc.ABCMeta', 'typing.TypingMeta', 'typing.GenericMeta')):
return False
return True
def get_func_def(op: Union[FuncDef, Decorator, OverloadedFuncDef]) -> FuncDef:
if isinstance(op, OverloadedFuncDef):
assert op.impl
op = op.impl
if isinstance(op, Decorator):
op = op.func
return op
def concrete_arg_kind(kind: ArgKind) -> ArgKind:
"""Find the concrete version of an arg kind that is being passed."""
if kind == ARG_OPT:
return ARG_POS
elif kind == ARG_NAMED_OPT:
return ARG_NAMED
else:
return kind
def is_constant(e: Expression) -> bool:
"""Check whether we allow an expression to appear as a default value.
We don't currently properly support storing the evaluated
values for default arguments and default attribute values, so
we restrict what expressions we allow. We allow literals of
primitives types, None, and references to Final global
variables.
"""
return (isinstance(e, (StrExpr, BytesExpr, IntExpr, FloatExpr))
or (isinstance(e, UnaryExpr) and e.op == '-'
and isinstance(e.expr, (IntExpr, FloatExpr)))
or (isinstance(e, TupleExpr)
and all(is_constant(e) for e in e.items))
or (isinstance(e, RefExpr) and e.kind == GDEF
and (e.fullname in ('builtins.True', 'builtins.False', 'builtins.None')
or (isinstance(e.node, Var) and e.node.is_final))))

View file

@ -0,0 +1,343 @@
"""Dispatcher used when transforming a mypy AST to the IR form.
mypyc.irbuild.builder and mypyc.irbuild.main are closely related.
"""
from typing_extensions import NoReturn
from mypy.nodes import (
MypyFile, FuncDef, ReturnStmt, AssignmentStmt, OpExpr,
IntExpr, NameExpr, Var, IfStmt, UnaryExpr, ComparisonExpr, WhileStmt, CallExpr,
IndexExpr, Block, ListExpr, ExpressionStmt, MemberExpr, ForStmt,
BreakStmt, ContinueStmt, ConditionalExpr, OperatorAssignmentStmt, TupleExpr, ClassDef,
Import, ImportFrom, ImportAll, DictExpr, StrExpr, CastExpr, TempNode,
PassStmt, PromoteExpr, AssignmentExpr, AwaitExpr, BackquoteExpr, AssertStmt, BytesExpr,
ComplexExpr, Decorator, DelStmt, DictionaryComprehension, EllipsisExpr, EnumCallExpr, ExecStmt,
FloatExpr, GeneratorExpr, GlobalDecl, LambdaExpr, ListComprehension, SetComprehension,
NamedTupleExpr, NewTypeExpr, NonlocalDecl, OverloadedFuncDef, PrintStmt, RaiseStmt,
RevealExpr, SetExpr, SliceExpr, StarExpr, SuperExpr, TryStmt, TypeAliasExpr, TypeApplication,
TypeVarExpr, TypedDictExpr, UnicodeExpr, WithStmt, YieldFromExpr, YieldExpr, ParamSpecExpr,
MatchStmt
)
from mypyc.ir.ops import Value
from mypyc.irbuild.builder import IRVisitor, IRBuilder, UnsupportedException
from mypyc.irbuild.classdef import transform_class_def
from mypyc.irbuild.function import (
transform_func_def,
transform_overloaded_func_def,
transform_decorator,
transform_lambda_expr,
transform_yield_expr,
transform_yield_from_expr,
transform_await_expr,
)
from mypyc.irbuild.statement import (
transform_block,
transform_expression_stmt,
transform_return_stmt,
transform_assignment_stmt,
transform_operator_assignment_stmt,
transform_import,
transform_import_from,
transform_import_all,
transform_if_stmt,
transform_while_stmt,
transform_for_stmt,
transform_break_stmt,
transform_continue_stmt,
transform_raise_stmt,
transform_try_stmt,
transform_with_stmt,
transform_assert_stmt,
transform_del_stmt,
)
from mypyc.irbuild.expression import (
transform_name_expr,
transform_member_expr,
transform_super_expr,
transform_call_expr,
transform_unary_expr,
transform_op_expr,
transform_index_expr,
transform_conditional_expr,
transform_int_expr,
transform_float_expr,
transform_complex_expr,
transform_comparison_expr,
transform_str_expr,
transform_bytes_expr,
transform_ellipsis,
transform_list_expr,
transform_tuple_expr,
transform_dict_expr,
transform_set_expr,
transform_list_comprehension,
transform_set_comprehension,
transform_dictionary_comprehension,
transform_slice_expr,
transform_generator_expr,
transform_assignment_expr,
)
class IRBuilderVisitor(IRVisitor):
"""Mypy node visitor that dispatches to node transform implementations.
This class should have no non-trivial logic.
This visitor is separated from the rest of code to improve modularity and
to avoid import cycles.
This is based on the visitor pattern
(https://en.wikipedia.org/wiki/Visitor_pattern).
"""
# This gets passed to all the implementations and contains all the
# state and many helpers. The attribute is initialized outside
# this class since this class and IRBuilder form a reference loop.
builder: IRBuilder
def visit_mypy_file(self, mypyfile: MypyFile) -> None:
assert False, "use transform_mypy_file instead"
def visit_class_def(self, cdef: ClassDef) -> None:
transform_class_def(self.builder, cdef)
def visit_import(self, node: Import) -> None:
transform_import(self.builder, node)
def visit_import_from(self, node: ImportFrom) -> None:
transform_import_from(self.builder, node)
def visit_import_all(self, node: ImportAll) -> None:
transform_import_all(self.builder, node)
def visit_func_def(self, fdef: FuncDef) -> None:
transform_func_def(self.builder, fdef)
def visit_overloaded_func_def(self, o: OverloadedFuncDef) -> None:
transform_overloaded_func_def(self.builder, o)
def visit_decorator(self, dec: Decorator) -> None:
transform_decorator(self.builder, dec)
def visit_block(self, block: Block) -> None:
transform_block(self.builder, block)
# Statements
def visit_expression_stmt(self, stmt: ExpressionStmt) -> None:
transform_expression_stmt(self.builder, stmt)
def visit_return_stmt(self, stmt: ReturnStmt) -> None:
transform_return_stmt(self.builder, stmt)
def visit_assignment_stmt(self, stmt: AssignmentStmt) -> None:
transform_assignment_stmt(self.builder, stmt)
def visit_operator_assignment_stmt(self, stmt: OperatorAssignmentStmt) -> None:
transform_operator_assignment_stmt(self.builder, stmt)
def visit_if_stmt(self, stmt: IfStmt) -> None:
transform_if_stmt(self.builder, stmt)
def visit_while_stmt(self, stmt: WhileStmt) -> None:
transform_while_stmt(self.builder, stmt)
def visit_for_stmt(self, stmt: ForStmt) -> None:
transform_for_stmt(self.builder, stmt)
def visit_break_stmt(self, stmt: BreakStmt) -> None:
transform_break_stmt(self.builder, stmt)
def visit_continue_stmt(self, stmt: ContinueStmt) -> None:
transform_continue_stmt(self.builder, stmt)
def visit_raise_stmt(self, stmt: RaiseStmt) -> None:
transform_raise_stmt(self.builder, stmt)
def visit_try_stmt(self, stmt: TryStmt) -> None:
transform_try_stmt(self.builder, stmt)
def visit_with_stmt(self, stmt: WithStmt) -> None:
transform_with_stmt(self.builder, stmt)
def visit_pass_stmt(self, stmt: PassStmt) -> None:
pass
def visit_assert_stmt(self, stmt: AssertStmt) -> None:
transform_assert_stmt(self.builder, stmt)
def visit_del_stmt(self, stmt: DelStmt) -> None:
transform_del_stmt(self.builder, stmt)
def visit_global_decl(self, stmt: GlobalDecl) -> None:
# Pure declaration -- no runtime effect
pass
def visit_nonlocal_decl(self, stmt: NonlocalDecl) -> None:
# Pure declaration -- no runtime effect
pass
def visit_match_stmt(self, stmt: MatchStmt) -> None:
self.bail("Match statements are not yet supported", stmt.line)
# Expressions
def visit_name_expr(self, expr: NameExpr) -> Value:
return transform_name_expr(self.builder, expr)
def visit_member_expr(self, expr: MemberExpr) -> Value:
return transform_member_expr(self.builder, expr)
def visit_super_expr(self, expr: SuperExpr) -> Value:
return transform_super_expr(self.builder, expr)
def visit_call_expr(self, expr: CallExpr) -> Value:
return transform_call_expr(self.builder, expr)
def visit_unary_expr(self, expr: UnaryExpr) -> Value:
return transform_unary_expr(self.builder, expr)
def visit_op_expr(self, expr: OpExpr) -> Value:
return transform_op_expr(self.builder, expr)
def visit_index_expr(self, expr: IndexExpr) -> Value:
return transform_index_expr(self.builder, expr)
def visit_conditional_expr(self, expr: ConditionalExpr) -> Value:
return transform_conditional_expr(self.builder, expr)
def visit_comparison_expr(self, expr: ComparisonExpr) -> Value:
return transform_comparison_expr(self.builder, expr)
def visit_int_expr(self, expr: IntExpr) -> Value:
return transform_int_expr(self.builder, expr)
def visit_float_expr(self, expr: FloatExpr) -> Value:
return transform_float_expr(self.builder, expr)
def visit_complex_expr(self, expr: ComplexExpr) -> Value:
return transform_complex_expr(self.builder, expr)
def visit_str_expr(self, expr: StrExpr) -> Value:
return transform_str_expr(self.builder, expr)
def visit_bytes_expr(self, expr: BytesExpr) -> Value:
return transform_bytes_expr(self.builder, expr)
def visit_ellipsis(self, expr: EllipsisExpr) -> Value:
return transform_ellipsis(self.builder, expr)
def visit_list_expr(self, expr: ListExpr) -> Value:
return transform_list_expr(self.builder, expr)
def visit_tuple_expr(self, expr: TupleExpr) -> Value:
return transform_tuple_expr(self.builder, expr)
def visit_dict_expr(self, expr: DictExpr) -> Value:
return transform_dict_expr(self.builder, expr)
def visit_set_expr(self, expr: SetExpr) -> Value:
return transform_set_expr(self.builder, expr)
def visit_list_comprehension(self, expr: ListComprehension) -> Value:
return transform_list_comprehension(self.builder, expr)
def visit_set_comprehension(self, expr: SetComprehension) -> Value:
return transform_set_comprehension(self.builder, expr)
def visit_dictionary_comprehension(self, expr: DictionaryComprehension) -> Value:
return transform_dictionary_comprehension(self.builder, expr)
def visit_slice_expr(self, expr: SliceExpr) -> Value:
return transform_slice_expr(self.builder, expr)
def visit_generator_expr(self, expr: GeneratorExpr) -> Value:
return transform_generator_expr(self.builder, expr)
def visit_lambda_expr(self, expr: LambdaExpr) -> Value:
return transform_lambda_expr(self.builder, expr)
def visit_yield_expr(self, expr: YieldExpr) -> Value:
return transform_yield_expr(self.builder, expr)
def visit_yield_from_expr(self, o: YieldFromExpr) -> Value:
return transform_yield_from_expr(self.builder, o)
def visit_await_expr(self, o: AwaitExpr) -> Value:
return transform_await_expr(self.builder, o)
def visit_assignment_expr(self, o: AssignmentExpr) -> Value:
return transform_assignment_expr(self.builder, o)
# Unimplemented constructs that shouldn't come up because they are py2 only
def visit_backquote_expr(self, o: BackquoteExpr) -> Value:
self.bail("Python 2 features are unsupported", o.line)
def visit_exec_stmt(self, o: ExecStmt) -> None:
self.bail("Python 2 features are unsupported", o.line)
def visit_print_stmt(self, o: PrintStmt) -> None:
self.bail("Python 2 features are unsupported", o.line)
def visit_unicode_expr(self, o: UnicodeExpr) -> Value:
self.bail("Python 2 features are unsupported", o.line)
# Constructs that shouldn't ever show up
def visit_enum_call_expr(self, o: EnumCallExpr) -> Value:
assert False, "can't compile analysis-only expressions"
def visit__promote_expr(self, o: PromoteExpr) -> Value:
assert False, "can't compile analysis-only expressions"
def visit_namedtuple_expr(self, o: NamedTupleExpr) -> Value:
assert False, "can't compile analysis-only expressions"
def visit_newtype_expr(self, o: NewTypeExpr) -> Value:
assert False, "can't compile analysis-only expressions"
def visit_temp_node(self, o: TempNode) -> Value:
assert False, "can't compile analysis-only expressions"
def visit_type_alias_expr(self, o: TypeAliasExpr) -> Value:
assert False, "can't compile analysis-only expressions"
def visit_type_application(self, o: TypeApplication) -> Value:
assert False, "can't compile analysis-only expressions"
def visit_type_var_expr(self, o: TypeVarExpr) -> Value:
assert False, "can't compile analysis-only expressions"
def visit_paramspec_expr(self, o: ParamSpecExpr) -> Value:
assert False, "can't compile analysis-only expressions"
def visit_typeddict_expr(self, o: TypedDictExpr) -> Value:
assert False, "can't compile analysis-only expressions"
def visit_reveal_expr(self, o: RevealExpr) -> Value:
assert False, "can't compile analysis-only expressions"
def visit_var(self, o: Var) -> None:
assert False, "can't compile Var; should have been handled already?"
def visit_cast_expr(self, o: CastExpr) -> Value:
assert False, "CastExpr should have been handled in CallExpr"
def visit_star_expr(self, o: StarExpr) -> Value:
assert False, "should have been handled in Tuple/List/Set/DictExpr or CallExpr"
# Helpers
def bail(self, msg: str, line: int) -> NoReturn:
"""Reports an error and aborts compilation up until the last accept() call
(accept() catches the UnsupportedException and keeps on
processing. This allows errors to be non-blocking without always
needing to write handling for them.
"""
self.builder.error(msg, line)
raise UnsupportedException()

View file

@ -0,0 +1,75 @@
"""Compute vtables of native (extension) classes."""
import itertools
from mypyc.ir.class_ir import ClassIR, VTableEntries, VTableMethod
from mypyc.sametype import is_same_method_signature
def compute_vtable(cls: ClassIR) -> None:
"""Compute the vtable structure for a class."""
if cls.vtable is not None:
return
if not cls.is_generated:
cls.has_dict = any(x.inherits_python for x in cls.mro)
for t in cls.mro[1:]:
# Make sure all ancestors are processed first
compute_vtable(t)
# Merge attributes from traits into the class
if not t.is_trait:
continue
for name, typ in t.attributes.items():
if not cls.is_trait and not any(name in b.attributes for b in cls.base_mro):
cls.attributes[name] = typ
cls.vtable = {}
if cls.base:
assert cls.base.vtable is not None
cls.vtable.update(cls.base.vtable)
cls.vtable_entries = specialize_parent_vtable(cls, cls.base)
# Include the vtable from the parent classes, but handle method overrides.
entries = cls.vtable_entries
all_traits = [t for t in cls.mro if t.is_trait]
for t in [cls] + cls.traits:
for fn in itertools.chain(t.methods.values()):
# TODO: don't generate a new entry when we overload without changing the type
if fn == cls.get_method(fn.name):
cls.vtable[fn.name] = len(entries)
# If the class contains a glue method referring to itself, that is a
# shadow glue method to support interpreted subclasses.
shadow = cls.glue_methods.get((cls, fn.name))
entries.append(VTableMethod(t, fn.name, fn, shadow))
# Compute vtables for all of the traits that the class implements
if not cls.is_trait:
for trait in all_traits:
compute_vtable(trait)
cls.trait_vtables[trait] = specialize_parent_vtable(cls, trait)
def specialize_parent_vtable(cls: ClassIR, parent: ClassIR) -> VTableEntries:
"""Generate the part of a vtable corresponding to a parent class or trait"""
updated = []
for entry in parent.vtable_entries:
# Find the original method corresponding to this vtable entry.
# (This may not be the method in the entry, if it was overridden.)
orig_parent_method = entry.cls.get_method(entry.name)
assert orig_parent_method
method_cls = cls.get_method_and_class(entry.name)
if method_cls:
child_method, defining_cls = method_cls
# TODO: emit a wrapper for __init__ that raises or something
if (is_same_method_signature(orig_parent_method.sig, child_method.sig)
or orig_parent_method.name == '__init__'):
entry = VTableMethod(entry.cls, entry.name, child_method, entry.shadow_method)
else:
entry = VTableMethod(entry.cls, entry.name,
defining_cls.glue_methods[(entry.cls, entry.name)],
entry.shadow_method)
updated.append(entry)
return updated