- 8 DB models (services, incidents, monitors, subscribers, etc.) - Full CRUD API for services, incidents, monitors - Public status page with live data - Incident detail page with timeline - API key authentication - Uptime monitoring scheduler - 13 tests passing - TECHNICAL_DESIGN.md with full spec
900 lines
35 KiB
Python
900 lines
35 KiB
Python
"""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 __future__ import annotations
|
|
|
|
from collections import defaultdict
|
|
from collections.abc import Iterable
|
|
from typing import Final, NamedTuple
|
|
|
|
from mypy.build import Graph
|
|
from mypy.nodes import (
|
|
ARG_STAR,
|
|
ARG_STAR2,
|
|
CallExpr,
|
|
ClassDef,
|
|
Decorator,
|
|
Expression,
|
|
FuncDef,
|
|
MemberExpr,
|
|
MypyFile,
|
|
NameExpr,
|
|
OverloadedFuncDef,
|
|
RefExpr,
|
|
SymbolNode,
|
|
TypeInfo,
|
|
Var,
|
|
)
|
|
from mypy.semanal import refers_to_fullname
|
|
from mypy.traverser import TraverserVisitor
|
|
from mypy.types import Instance, Type, get_proper_type
|
|
from mypyc.common import FAST_PREFIX, PROPSET_PREFIX, SELF_NAME, get_id_from_name
|
|
from mypyc.crash import catch_errors
|
|
from mypyc.errors import Errors
|
|
from mypyc.ir.class_ir import ClassIR
|
|
from mypyc.ir.func_ir import (
|
|
FUNC_CLASSMETHOD,
|
|
FUNC_NORMAL,
|
|
FUNC_STATICMETHOD,
|
|
FuncDecl,
|
|
FuncSignature,
|
|
RuntimeArg,
|
|
)
|
|
from mypyc.ir.ops import DeserMaps
|
|
from mypyc.ir.rtypes import (
|
|
RInstance,
|
|
RType,
|
|
dict_rprimitive,
|
|
none_rprimitive,
|
|
object_pointer_rprimitive,
|
|
object_rprimitive,
|
|
tuple_rprimitive,
|
|
)
|
|
from mypyc.irbuild.mapper import Mapper
|
|
from mypyc.irbuild.util import (
|
|
get_func_def,
|
|
get_mypyc_attrs,
|
|
is_dataclass,
|
|
is_extension_class,
|
|
is_trait,
|
|
)
|
|
from mypyc.options import CompilerOptions
|
|
from mypyc.sametype import is_same_type
|
|
|
|
GENERATOR_HELPER_NAME: Final = "__mypyc_generator_helper__"
|
|
|
|
|
|
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,
|
|
is_final_class=cdef.info.is_final,
|
|
)
|
|
class_ir.is_ext_class = is_extension_class(module.path, cdef, errors)
|
|
if class_ir.is_ext_class:
|
|
class_ir.deletable = cdef.info.deletable_attributes.copy()
|
|
# 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
|
|
mapper.symbol_fullnames.add(class_ir.fullname)
|
|
class_ir.is_enum = cdef.info.is_enum and len(cdef.info.enum_members) > 0
|
|
|
|
# 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, options)
|
|
else:
|
|
prepare_non_ext_class_def(
|
|
module.path, module.fullname, cdef, errors, mapper, options
|
|
)
|
|
|
|
# Prepare implicit attribute accessors as needed if an attribute overrides a property.
|
|
for module, cdef in classes:
|
|
class_ir = mapper.type_to_ir[cdef.info]
|
|
if class_ir.is_ext_class:
|
|
prepare_implicit_property_accessors(cdef.info, class_ir, module.fullname, 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. This doesn't include nested functions!
|
|
for module in modules:
|
|
for func in get_module_func_defs(module):
|
|
prepare_func_def(module.fullname, None, func, mapper, options)
|
|
# TODO: what else?
|
|
|
|
# Check for incompatible attribute definitions that were not
|
|
# flagged by mypy but can't be supported when compiling.
|
|
for module, cdef in classes:
|
|
class_ir = mapper.type_to_ir[cdef.info]
|
|
for attr in class_ir.attributes:
|
|
for base_ir in class_ir.mro[1:]:
|
|
if attr in base_ir.attributes:
|
|
if not is_same_type(class_ir.attributes[attr], base_ir.attributes[attr]):
|
|
node = cdef.info.names[attr].node
|
|
assert node is not None
|
|
kind = "trait" if base_ir.is_trait else "class"
|
|
errors.error(
|
|
f'Type of "{attr}" is incompatible with '
|
|
f'definition in {kind} "{base_ir.name}"',
|
|
module.path,
|
|
node.line,
|
|
)
|
|
|
|
|
|
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 node in module.names.values():
|
|
if (
|
|
isinstance(node.node, TypeInfo)
|
|
and is_from_module(node.node, module)
|
|
and not node.node.is_newtype
|
|
and not node.node.is_named_tuple
|
|
and node.node.typeddict_type is None
|
|
):
|
|
ir = deser_ctx.classes[node.node.fullname]
|
|
mapper.type_to_ir[node.node] = ir
|
|
mapper.symbol_fullnames.add(node.node.fullname)
|
|
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 node in module.names.values():
|
|
# 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: str | None,
|
|
fdef: FuncDef,
|
|
mapper: Mapper,
|
|
options: CompilerOptions,
|
|
) -> FuncDecl:
|
|
kind = (
|
|
FUNC_CLASSMETHOD
|
|
if fdef.is_class
|
|
else (FUNC_STATICMETHOD if fdef.is_static else FUNC_NORMAL)
|
|
)
|
|
sig = mapper.fdef_to_sig(fdef, options.strict_dunders_typing)
|
|
decl = FuncDecl(
|
|
fdef.name,
|
|
class_name,
|
|
module_name,
|
|
sig,
|
|
kind,
|
|
is_generator=fdef.is_generator,
|
|
is_coroutine=fdef.is_coroutine,
|
|
)
|
|
mapper.func_to_decl[fdef] = decl
|
|
return decl
|
|
|
|
|
|
def create_generator_class_for_func(
|
|
module_name: str, class_name: str | None, fdef: FuncDef, mapper: Mapper, name_suffix: str = ""
|
|
) -> ClassIR:
|
|
"""For a generator/async function, declare a generator class.
|
|
|
|
Each generator and async function gets a dedicated class that implements the
|
|
generator protocol with generated methods.
|
|
"""
|
|
assert fdef.is_coroutine or fdef.is_generator
|
|
name = "_".join(x for x in [fdef.name, class_name] if x) + "_gen" + name_suffix
|
|
cir = ClassIR(name, module_name, is_generated=True, is_final_class=class_name is None)
|
|
cir.reuse_freed_instance = True
|
|
mapper.fdef_to_generator[fdef] = cir
|
|
|
|
helper_sig = FuncSignature(
|
|
(
|
|
RuntimeArg(SELF_NAME, object_rprimitive),
|
|
RuntimeArg("type", object_rprimitive),
|
|
RuntimeArg("value", object_rprimitive),
|
|
RuntimeArg("traceback", object_rprimitive),
|
|
RuntimeArg("arg", object_rprimitive),
|
|
# If non-NULL, used to store return value instead of raising StopIteration(retv)
|
|
RuntimeArg("stop_iter_ptr", object_pointer_rprimitive),
|
|
),
|
|
object_rprimitive,
|
|
)
|
|
|
|
# The implementation of most generator functionality is behind this magic method.
|
|
helper_fn_decl = FuncDecl(GENERATOR_HELPER_NAME, name, module_name, helper_sig, internal=True)
|
|
cir.method_decls[helper_fn_decl.name] = helper_fn_decl
|
|
return cir
|
|
|
|
|
|
def prepare_method_def(
|
|
ir: ClassIR,
|
|
module_name: str,
|
|
cdef: ClassDef,
|
|
mapper: Mapper,
|
|
node: FuncDef | Decorator,
|
|
options: CompilerOptions,
|
|
) -> None:
|
|
if isinstance(node, FuncDef):
|
|
ir.method_decls[node.name] = prepare_func_def(
|
|
module_name, cdef.name, node, mapper, options
|
|
)
|
|
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, options)
|
|
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
|
|
# Making the argument implicitly positional-only avoids unnecessary glue methods
|
|
decl.sig.args[1].pos_only = 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 prepare_fast_path(
|
|
ir: ClassIR,
|
|
module_name: str,
|
|
cdef: ClassDef,
|
|
mapper: Mapper,
|
|
node: SymbolNode | None,
|
|
options: CompilerOptions,
|
|
) -> None:
|
|
"""Add fast (direct) variants of methods in non-extension classes."""
|
|
if ir.is_enum:
|
|
# We check that non-empty enums are implicitly final in mypy, so we
|
|
# can generate direct calls to enum methods.
|
|
if isinstance(node, OverloadedFuncDef):
|
|
if node.is_property:
|
|
return
|
|
node = node.impl
|
|
if not isinstance(node, FuncDef):
|
|
# TODO: support decorated methods (at least @classmethod and @staticmethod).
|
|
return
|
|
# The simplest case is a regular or overloaded method without decorators. In this
|
|
# case we can generate practically identical IR method body, but with a signature
|
|
# suitable for direct calls (usual non-extension class methods are converted to
|
|
# callable classes, and thus have an extra __mypyc_self__ argument).
|
|
name = FAST_PREFIX + node.name
|
|
sig = mapper.fdef_to_sig(node, options.strict_dunders_typing)
|
|
decl = FuncDecl(name, cdef.name, module_name, sig, FUNC_NORMAL)
|
|
ir.method_decls[name] = decl
|
|
return
|
|
|
|
|
|
def is_valid_multipart_property_def(prop: OverloadedFuncDef) -> bool:
|
|
# Checks to ensure supported property decorator semantics
|
|
if len(prop.items) != 2:
|
|
return False
|
|
|
|
getter = prop.items[0]
|
|
setter = prop.items[1]
|
|
|
|
return (
|
|
isinstance(getter, Decorator)
|
|
and isinstance(setter, Decorator)
|
|
and getter.func.is_property
|
|
and len(setter.decorators) == 1
|
|
and isinstance(setter.decorators[0], MemberExpr)
|
|
and setter.decorators[0].name == "setter"
|
|
)
|
|
|
|
|
|
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,
|
|
options: CompilerOptions,
|
|
) -> None:
|
|
"""Populate the interface-level information in a class IR.
|
|
|
|
This includes attribute and method declarations, and the MRO, among other things, but
|
|
method bodies are generated in a later pass.
|
|
"""
|
|
|
|
ir = mapper.type_to_ir[cdef.info]
|
|
info = cdef.info
|
|
|
|
attrs, attrs_lines = get_mypyc_attrs(cdef, path, errors)
|
|
if attrs.get("allow_interpreted_subclasses") is True:
|
|
ir.allow_interpreted_subclasses = True
|
|
if attrs.get("serializable") is True:
|
|
# Supports copy.copy and pickle (including subclasses)
|
|
ir._serializable = True
|
|
|
|
if attrs.get("acyclic") is True:
|
|
ir.is_acyclic = True
|
|
|
|
free_list_len = attrs.get("free_list_len")
|
|
if free_list_len is not None:
|
|
line = attrs_lines["free_list_len"]
|
|
if ir.is_trait:
|
|
errors.error('"free_list_len" can\'t be used with traits', path, line)
|
|
if ir.allow_interpreted_subclasses:
|
|
errors.error(
|
|
'"free_list_len" can\'t be used in a class that allows interpreted subclasses',
|
|
path,
|
|
line,
|
|
)
|
|
if free_list_len == 1:
|
|
ir.reuse_freed_instance = True
|
|
else:
|
|
errors.error(f'Unsupported value for "free_list_len": {free_list_len}', path, line)
|
|
|
|
# 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
|
|
)
|
|
errors.note(
|
|
"Potential workaround: @mypy_extensions.mypyc_attr(native_class=False)",
|
|
path,
|
|
cdef.line,
|
|
)
|
|
errors.note(
|
|
"https://mypyc.readthedocs.io/en/stable/native_classes.html#defining-non-native-classes",
|
|
path,
|
|
cdef.line,
|
|
)
|
|
|
|
# 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 len(bases) > 1 and any(not c.is_trait for c in bases) and bases[0].is_trait:
|
|
# If the first base is a non-trait, don't ever error here. While it is correct
|
|
# to error if a trait comes before the next non-trait base (e.g. non-trait, trait,
|
|
# non-trait), it's pointless, confusing noise from the bigger issue: multiple
|
|
# inheritance is *not* supported.
|
|
errors.error("Non-trait base must appear first in parent list", path, cdef.line)
|
|
ir.traits = [c for c in bases if c.is_trait]
|
|
|
|
mro = [] # All mypyc base classes
|
|
base_mro = [] # Non-trait mypyc base classes
|
|
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
|
|
|
|
prepare_methods_and_attributes(cdef, ir, path, module_name, errors, mapper, options)
|
|
prepare_init_method(cdef, ir, module_name, mapper)
|
|
|
|
for base in bases:
|
|
if base.children is not None:
|
|
base.children.append(ir)
|
|
|
|
if is_dataclass(cdef):
|
|
ir.is_augmented = True
|
|
|
|
|
|
def prepare_methods_and_attributes(
|
|
cdef: ClassDef,
|
|
ir: ClassIR,
|
|
path: str,
|
|
module_name: str,
|
|
errors: Errors,
|
|
mapper: Mapper,
|
|
options: CompilerOptions,
|
|
) -> None:
|
|
"""Populate attribute and method declarations."""
|
|
info = cdef.info
|
|
for name, node in 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__"):
|
|
attr_rtype = mapper.type_to_rtype(node.node.type)
|
|
if ir.is_trait and attr_rtype.error_overlap:
|
|
# Traits don't have attribute definedness bitmaps, so use
|
|
# property accessor methods to access attributes that need them.
|
|
# We will generate accessor implementations that use the class bitmap
|
|
# for any concrete subclasses.
|
|
add_getter_declaration(ir, name, attr_rtype, module_name)
|
|
add_setter_declaration(ir, name, attr_rtype, module_name)
|
|
ir.attributes[name] = attr_rtype
|
|
elif isinstance(node.node, (FuncDef, Decorator)):
|
|
prepare_method_def(ir, module_name, cdef, mapper, node.node, options)
|
|
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, options)
|
|
else:
|
|
errors.error("Unsupported property decorator semantics", path, cdef.line)
|
|
|
|
# Handle case for regular function overload
|
|
else:
|
|
if not node.node.impl:
|
|
errors.error(
|
|
"Overloads without implementation are not supported", path, cdef.line
|
|
)
|
|
else:
|
|
prepare_method_def(ir, module_name, cdef, mapper, node.node.impl, options)
|
|
|
|
if ir.builtin_base:
|
|
ir.attributes.clear()
|
|
|
|
|
|
def prepare_implicit_property_accessors(
|
|
info: TypeInfo, ir: ClassIR, module_name: str, mapper: Mapper
|
|
) -> None:
|
|
concrete_attributes = set()
|
|
for base in ir.base_mro:
|
|
for name, attr_rtype in base.attributes.items():
|
|
concrete_attributes.add(name)
|
|
add_property_methods_for_attribute_if_needed(
|
|
info, ir, name, attr_rtype, module_name, mapper
|
|
)
|
|
for base in ir.mro[1:]:
|
|
if base.is_trait:
|
|
for name, attr_rtype in base.attributes.items():
|
|
if name not in concrete_attributes:
|
|
add_property_methods_for_attribute_if_needed(
|
|
info, ir, name, attr_rtype, module_name, mapper
|
|
)
|
|
|
|
|
|
def add_property_methods_for_attribute_if_needed(
|
|
info: TypeInfo,
|
|
ir: ClassIR,
|
|
attr_name: str,
|
|
attr_rtype: RType,
|
|
module_name: str,
|
|
mapper: Mapper,
|
|
) -> None:
|
|
"""Add getter and/or setter for attribute if defined as property in a base class.
|
|
|
|
Only add declarations. The body IR will be synthesized later during irbuild.
|
|
"""
|
|
for base in info.mro[1:]:
|
|
if base in mapper.type_to_ir:
|
|
base_ir = mapper.type_to_ir[base]
|
|
n = base.names.get(attr_name)
|
|
if n is None:
|
|
continue
|
|
node = n.node
|
|
if isinstance(node, Decorator) and node.name not in ir.method_decls:
|
|
# Defined as a read-only property in base class/trait
|
|
add_getter_declaration(ir, attr_name, attr_rtype, module_name)
|
|
elif isinstance(node, OverloadedFuncDef) and is_valid_multipart_property_def(node):
|
|
# Defined as a read-write property in base class/trait
|
|
add_getter_declaration(ir, attr_name, attr_rtype, module_name)
|
|
add_setter_declaration(ir, attr_name, attr_rtype, module_name)
|
|
elif base_ir.is_trait and attr_rtype.error_overlap:
|
|
add_getter_declaration(ir, attr_name, attr_rtype, module_name)
|
|
add_setter_declaration(ir, attr_name, attr_rtype, module_name)
|
|
|
|
|
|
def add_getter_declaration(
|
|
ir: ClassIR, attr_name: str, attr_rtype: RType, module_name: str
|
|
) -> None:
|
|
self_arg = RuntimeArg("self", RInstance(ir), pos_only=True)
|
|
sig = FuncSignature([self_arg], attr_rtype)
|
|
decl = FuncDecl(attr_name, ir.name, module_name, sig, FUNC_NORMAL)
|
|
decl.is_prop_getter = True
|
|
decl.implicit = True # Triggers synthesization
|
|
ir.method_decls[attr_name] = decl
|
|
ir.property_types[attr_name] = attr_rtype # TODO: Needed??
|
|
|
|
|
|
def add_setter_declaration(
|
|
ir: ClassIR, attr_name: str, attr_rtype: RType, module_name: str
|
|
) -> None:
|
|
self_arg = RuntimeArg("self", RInstance(ir), pos_only=True)
|
|
value_arg = RuntimeArg("value", attr_rtype, pos_only=True)
|
|
sig = FuncSignature([self_arg, value_arg], none_rprimitive)
|
|
setter_name = PROPSET_PREFIX + attr_name
|
|
decl = FuncDecl(setter_name, ir.name, module_name, sig, FUNC_NORMAL)
|
|
decl.is_prop_setter = True
|
|
decl.implicit = True # Triggers synthesization
|
|
ir.method_decls[setter_name] = decl
|
|
|
|
|
|
def check_matching_args(init_sig: FuncSignature, new_sig: FuncSignature) -> bool:
|
|
num_init_args = len(init_sig.args) - init_sig.num_bitmap_args
|
|
num_new_args = len(new_sig.args) - new_sig.num_bitmap_args
|
|
if num_init_args != num_new_args:
|
|
return False
|
|
|
|
for idx in range(1, num_init_args):
|
|
init_arg = init_sig.args[idx]
|
|
new_arg = new_sig.args[idx]
|
|
if init_arg.type != new_arg.type:
|
|
return False
|
|
|
|
if init_arg.kind != new_arg.kind:
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def prepare_init_method(cdef: ClassDef, ir: ClassIR, module_name: str, mapper: Mapper) -> None:
|
|
# Set up a constructor decl
|
|
init_node = cdef.info["__init__"].node
|
|
|
|
new_node: SymbolNode | None = None
|
|
new_symbol = cdef.info.get("__new__")
|
|
# We are only interested in __new__ method defined in a user-defined class,
|
|
# so we ignore it if it comes from a builtin type. It's usually builtins.object
|
|
# but could also be builtins.type for metaclasses so we detect the prefix which
|
|
# matches both.
|
|
if new_symbol and new_symbol.fullname and not new_symbol.fullname.startswith("builtins."):
|
|
new_node = new_symbol.node
|
|
if isinstance(new_node, (Decorator, OverloadedFuncDef)):
|
|
new_node = get_func_def(new_node)
|
|
if not ir.is_trait and not ir.builtin_base and isinstance(init_node, FuncDef):
|
|
init_sig = mapper.fdef_to_sig(init_node, True)
|
|
args_match = True
|
|
if isinstance(new_node, FuncDef):
|
|
new_sig = mapper.fdef_to_sig(new_node, True)
|
|
args_match = check_matching_args(init_sig, new_sig)
|
|
|
|
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"
|
|
) or not args_match:
|
|
init_sig = FuncSignature(
|
|
[
|
|
init_sig.args[0],
|
|
RuntimeArg("args", tuple_rprimitive, ARG_STAR),
|
|
RuntimeArg("kwargs", dict_rprimitive, ARG_STAR2),
|
|
],
|
|
init_sig.ret_type,
|
|
)
|
|
|
|
last_arg = len(init_sig.args) - init_sig.num_bitmap_args
|
|
ctor_sig = FuncSignature(init_sig.args[1:last_arg], RInstance(ir))
|
|
ir.ctor = FuncDecl(cdef.name, None, module_name, ctor_sig)
|
|
mapper.func_to_decl[cdef.info] = ir.ctor
|
|
|
|
|
|
def prepare_non_ext_class_def(
|
|
path: str,
|
|
module_name: str,
|
|
cdef: ClassDef,
|
|
errors: Errors,
|
|
mapper: Mapper,
|
|
options: CompilerOptions,
|
|
) -> None:
|
|
ir = mapper.type_to_ir[cdef.info]
|
|
info = cdef.info
|
|
|
|
for node in info.names.values():
|
|
if isinstance(node.node, (FuncDef, Decorator)):
|
|
prepare_method_def(ir, module_name, cdef, mapper, node.node, options)
|
|
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, options)
|
|
# Handle case for regular function overload
|
|
else:
|
|
prepare_method_def(ir, module_name, cdef, mapper, get_func_def(node.node), options)
|
|
|
|
prepare_fast_path(ir, module_name, cdef, mapper, node.node, options)
|
|
|
|
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
|
|
self.func_stack_depth = 0
|
|
|
|
def visit_func_def(self, o: FuncDef) -> None:
|
|
self.func_stack_depth += 1
|
|
super().visit_func_def(o)
|
|
self.func_stack_depth -= 1
|
|
|
|
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: int | None = None
|
|
for i, d in enumerate(decorators_to_store):
|
|
impl = get_singledispatch_register_call_info(d, dec.func)
|
|
if impl is not None:
|
|
if self.func_stack_depth > 0:
|
|
self.errors.error(
|
|
"Registering nested functions not supported", self.current_path, d.line
|
|
)
|
|
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"):
|
|
if self.func_stack_depth > 0:
|
|
self.errors.error(
|
|
"Nested singledispatch functions not supported",
|
|
self.current_path,
|
|
d.line,
|
|
)
|
|
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
|
|
) -> RegisteredImpl | None:
|
|
# @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
|
|
) -> RegisteredImpl | None:
|
|
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
|
|
|
|
|
|
def adjust_generator_classes_of_methods(mapper: Mapper) -> None:
|
|
"""Make optimizations and adjustments to generated generator classes of methods.
|
|
|
|
This is a separate pass after type map has been built, since we need all classes
|
|
to be processed to analyze class hierarchies.
|
|
"""
|
|
|
|
generator_methods = []
|
|
|
|
for fdef, fn_ir in mapper.func_to_decl.items():
|
|
if isinstance(fdef, FuncDef) and (fdef.is_coroutine or fdef.is_generator):
|
|
gen_ir = create_generator_class_for_func(
|
|
fn_ir.module_name, fn_ir.class_name, fdef, mapper
|
|
)
|
|
# TODO: We could probably support decorators sometimes (static and class method?)
|
|
if not fdef.is_decorated:
|
|
name = fn_ir.name
|
|
precise_ret_type = True
|
|
if fn_ir.class_name is not None:
|
|
class_ir = mapper.type_to_ir[fdef.info]
|
|
subcls = class_ir.subclasses()
|
|
if subcls is None:
|
|
# Override could be of a different type, so we can't make assumptions.
|
|
precise_ret_type = False
|
|
elif class_ir.is_trait:
|
|
# Give up on traits. We could possibly have an abstract base class
|
|
# for generator return types to make this use precise types.
|
|
precise_ret_type = False
|
|
else:
|
|
for s in subcls:
|
|
if name in s.method_decls:
|
|
m = s.method_decls[name]
|
|
if (
|
|
m.is_generator != fn_ir.is_generator
|
|
or m.is_coroutine != fn_ir.is_coroutine
|
|
):
|
|
# Override is of a different kind, and the optimization
|
|
# to use a precise generator return type doesn't work.
|
|
precise_ret_type = False
|
|
else:
|
|
class_ir = None
|
|
|
|
if precise_ret_type:
|
|
# Give a more precise type for generators, so that we can optimize
|
|
# code that uses them. They return a generator object, which has a
|
|
# specific class. Without this, the type would have to be 'object'.
|
|
fn_ir.sig.ret_type = RInstance(gen_ir)
|
|
if fn_ir.bound_sig:
|
|
fn_ir.bound_sig.ret_type = RInstance(gen_ir)
|
|
if class_ir is not None:
|
|
if class_ir.is_method_final(name):
|
|
gen_ir.is_final_class = True
|
|
generator_methods.append((name, class_ir, gen_ir))
|
|
|
|
new_bases = {}
|
|
|
|
for name, class_ir, gen in generator_methods:
|
|
# For generator methods, we need to have subclass generator classes inherit from
|
|
# baseclass generator classes when there are overrides to maintain LSP.
|
|
base = class_ir.real_base()
|
|
if base is not None:
|
|
if base.has_method(name):
|
|
base_sig = base.method_sig(name)
|
|
if isinstance(base_sig.ret_type, RInstance):
|
|
base_gen = base_sig.ret_type.class_ir
|
|
new_bases[gen] = base_gen
|
|
|
|
# Add generator inheritance relationships by adjusting MROs.
|
|
for deriv, base in new_bases.items():
|
|
if base.children is not None:
|
|
base.children.append(deriv)
|
|
while True:
|
|
deriv.mro.append(base)
|
|
deriv.base_mro.append(base)
|
|
if base not in new_bases:
|
|
break
|
|
base = new_bases[base]
|