77from typing import Final
88
99from mypy .nodes import (
10+ ARG_POS ,
1011 EXCLUDED_ENUM_ATTRIBUTES ,
1112 TYPE_VAR_TUPLE_KIND ,
1213 AssignmentStmt ,
2122 NameExpr ,
2223 OverloadedFuncDef ,
2324 PassStmt ,
24- RefExpr ,
2525 StrExpr ,
2626 TempNode ,
2727 TypeInfo ,
2828 TypeParam ,
2929 is_class_var ,
3030)
3131from mypy .types import Instance , UnboundType , get_proper_type
32- from mypyc .common import PROPSET_PREFIX
32+ from mypyc .common import MYPYC_DEFAULTS_SETUP , PROPSET_PREFIX
3333from mypyc .ir .class_ir import ClassIR , NonExtClassInfo
3434from mypyc .ir .func_ir import FuncDecl , FuncSignature
3535from mypyc .ir .ops import (
4848 TupleSet ,
4949 Value ,
5050)
51- from mypyc .ir .rtypes import (
52- RType ,
53- bool_rprimitive ,
54- dict_rprimitive ,
55- is_none_rprimitive ,
56- is_object_rprimitive ,
57- is_optional_type ,
58- object_rprimitive ,
59- )
51+ from mypyc .ir .rtypes import RType , bool_rprimitive , dict_rprimitive , object_rprimitive
6052from mypyc .irbuild .builder import IRBuilder , create_type_params
6153from mypyc .irbuild .function import (
6254 gen_property_getter_ir ,
6658 load_type ,
6759)
6860from mypyc .irbuild .prepare import GENERATOR_HELPER_NAME
69- from mypyc .irbuild .util import dataclass_type , get_func_def , is_constant , is_dataclass_decorator
61+ from mypyc .irbuild .util import (
62+ dataclass_type ,
63+ default_attr_name ,
64+ get_func_def ,
65+ is_constant ,
66+ is_dataclass_decorator ,
67+ )
7068from mypyc .primitives .dict_ops import dict_new_op , exact_dict_set_item_op
7169from mypyc .primitives .generic_ops import (
7270 iter_op ,
@@ -322,10 +320,6 @@ def __init__(self, builder: IRBuilder, cdef: ClassDef) -> None:
322320 def class_body_obj (self ) -> Value | None :
323321 return self .type_obj
324322
325- def skip_attr_default (self , name : str , stmt : AssignmentStmt ) -> bool :
326- """Controls whether to skip generating a default for an attribute."""
327- return False
328-
329323 def add_method (self , fdef : FuncDef ) -> None :
330324 handle_ext_method (self .builder , self .cdef , fdef )
331325
@@ -348,11 +342,18 @@ def finalize(self, ir: ClassIR) -> None:
348342 # Call __init_subclass__ after class attributes have been set
349343 self .builder .call_c (py_init_subclass_op , [self .type_obj ], self .cdef .line )
350344
351- attrs_with_defaults , default_assignments = find_attr_initializers (
352- self .builder , self .cdef , self .skip_attr_default
353- )
354- ir .attrs_with_defaults .update (attrs_with_defaults )
355- generate_attr_defaults_init (self .builder , self .cdef , default_assignments )
345+ # Under separate compilation, prepare.py pre-registers the decl iff
346+ # the class has its own default attribute assignments to emit, so we
347+ # can skip the body walk entirely when it isn't present. Without
348+ # separate compilation, find_attr_initializers walks the MRO so that
349+ # inherited defaults are reflected in ir.attrs_with_defaults (relied
350+ # on by the attribute-definedness analysis), so we always run it.
351+ if not self .builder .options .separate or MYPYC_DEFAULTS_SETUP in ir .method_decls :
352+ attrs_with_defaults , default_assignments = find_attr_initializers (
353+ self .builder , self .cdef
354+ )
355+ ir .attrs_with_defaults .update (attrs_with_defaults )
356+ generate_attr_defaults_init (self .builder , self .cdef , default_assignments )
356357 create_ne_from_eq (self .builder , self .cdef )
357358
358359
@@ -380,9 +381,6 @@ def create_non_ext_info(self) -> NonExtClassInfo:
380381 self .builder .add (LoadAddress (type_object_op .type , type_object_op .src , self .cdef .line )),
381382 )
382383
383- def skip_attr_default (self , name : str , stmt : AssignmentStmt ) -> bool :
384- return stmt .type is not None
385-
386384 def get_type_annotation (self , stmt : AssignmentStmt ) -> TypeInfo | None :
387385 # We populate __annotations__ because dataclasses uses it to determine
388386 # which attributes to compute on.
@@ -445,9 +443,6 @@ class AttrsClassBuilder(DataClassBuilder):
445443
446444 add_annotations_to_dict = False
447445
448- def skip_attr_default (self , name : str , stmt : AssignmentStmt ) -> bool :
449- return True
450-
451446 def get_type_annotation (self , stmt : AssignmentStmt ) -> TypeInfo | None :
452447 if isinstance (stmt .rvalue , CallExpr ):
453448 # find the type arg in `attr.ib(type=str)`
@@ -741,74 +736,100 @@ def add_non_ext_class_attr(
741736
742737
743738def find_attr_initializers (
744- builder : IRBuilder , cdef : ClassDef , skip : Callable [[ str , AssignmentStmt ], bool ] | None = None
739+ builder : IRBuilder , cdef : ClassDef
745740) -> tuple [set [str ], list [tuple [AssignmentStmt , str ]]]:
746741 """Find initializers of attributes in a class body.
747742
748- If provided, the skip arg should be a callable which will return whether
749- to skip generating a default for an attribute. It will be passed the name of
750- the attribute and the corresponding AssignmentStmt.
743+ Under separate compilation, only this class's own body is walked, and
744+ generate_attr_defaults_init emits a runtime call to the parent's
745+ __mypyc_defaults_setup so inherited defaults are produced by chaining,
746+ not by inlining. Walking the MRO here would break under separate=True
747+ with mypy's incremental cache: a base class loaded from the cache has
748+ an empty ClassDef.defs.body (mypy/nodes.py::ClassDef.serialize doesn't
749+ serialize the class body), so inherited assignments would be silently
750+ dropped and the subclass's __mypyc_defaults_setup would leave inherited
751+ slots in the "undefined" state at runtime.
752+
753+ Without separate compilation, all modules are parsed in the same pass
754+ and the MRO walk is safe; we keep the original inline-all behavior
755+ there as an optimization (no chain call needed for instance creation).
751756 """
752757 cls = builder .mapper .type_to_ir [cdef .info ]
753758 if cls .builtin_base :
754759 return set (), []
755760
756- attrs_with_defaults = set ()
761+ cls_type = dataclass_type (cdef )
762+ attrs_with_defaults : set [str ] = set ()
763+ default_assignments : list [tuple [AssignmentStmt , str ]] = []
757764
758- # Pull out all assignments in classes in the mro so we can initialize them
759765 # TODO: Support nested statements
760- default_assignments : list [tuple [AssignmentStmt , str ]] = []
761- for info in reversed (cdef .info .mro ):
762- if info not in builder .mapper .type_to_ir :
766+ if builder .options .separate :
767+ infos : list [TypeInfo ] = [cdef .info ]
768+ else :
769+ infos = list (reversed (cdef .info .mro ))
770+
771+ for info in infos :
772+ info_ir = builder .mapper .type_to_ir .get (info )
773+ if info_ir is None :
763774 continue
764775 for stmt in info .defn .defs .body :
765- if (
766- isinstance (stmt , AssignmentStmt )
767- and isinstance (stmt .lvalues [0 ], NameExpr )
768- and not is_class_var (stmt .lvalues [0 ])
769- and not isinstance (stmt .rvalue , TempNode )
770- ):
771- name = stmt .lvalues [0 ].name
772- if name == "__slots__" :
773- continue
774-
775- if name == "__deletable__" :
776- check_deletable_declaration (builder , cls , stmt .line )
777- continue
778-
779- if skip is not None and skip (name , stmt ):
780- continue
781-
782- attr_type = cls .attr_type (name )
783-
784- # If the attribute is initialized to None and type isn't optional,
785- # doesn't initialize it to anything (special case for "# type:" comments).
786- if isinstance (stmt .rvalue , RefExpr ) and stmt .rvalue .fullname == "builtins.None" :
787- if (
788- not is_optional_type (attr_type )
789- and not is_object_rprimitive (attr_type )
790- and not is_none_rprimitive (attr_type )
791- ):
792- continue
793-
794- attrs_with_defaults .add (name )
795- default_assignments .append ((stmt , info .module_name ))
776+ if not isinstance (stmt , AssignmentStmt ):
777+ continue
778+ name = default_attr_name (stmt , info_ir , cls_type )
779+ if name is None :
780+ continue
781+ attrs_with_defaults .add (name )
782+ default_assignments .append ((stmt , info .module_name ))
796783
797784 return attrs_with_defaults , default_assignments
798785
799786
800787def generate_attr_defaults_init (
801788 builder : IRBuilder , cdef : ClassDef , default_assignments : list [tuple [AssignmentStmt , str ]]
802789) -> None :
803- """Generate an initialization method for default attr values (from class vars)."""
804- if not default_assignments :
805- return
790+ """Generate an initialization method for default attr values (from class vars).
791+
792+ Under separate compilation, the emitted __mypyc_defaults_setup chains to
793+ the nearest ancestor that has the method (Python __init__ style), then
794+ sets only this class's own defaults; inherited defaults are produced by
795+ the chain at runtime. The ancestor lookup uses cls.mro[1:] and relies on
796+ prepare.py having registered the FuncDecl on every class that needs one
797+ before any IR build runs. IR build within a compilation group proceeds
798+ in filename order, so this class may be IR-built before its base, and a
799+ method_decls lookup that depended on the base having been IR-built first
800+ would miss. Without separate compilation, find_attr_initializers has
801+ already collected the full MRO's defaults into default_assignments, so
802+ we inline them all as before.
803+ """
806804 cls = builder .mapper .type_to_ir [cdef .info ]
807805 if cls .builtin_base :
808806 return
809807
810- with builder .enter_method (cls , "__mypyc_defaults_setup" , bool_rprimitive ):
808+ parent_with_defaults : ClassIR | None = None
809+ if builder .options .separate :
810+ for ancestor in cls .mro [1 :]:
811+ if MYPYC_DEFAULTS_SETUP in ancestor .method_decls :
812+ parent_with_defaults = ancestor
813+ break
814+
815+ if not default_assignments and parent_with_defaults is None :
816+ return
817+
818+ with builder .enter_method (cls , MYPYC_DEFAULTS_SETUP , bool_rprimitive ):
811819 self_var = builder .self ()
820+
821+ # Chain to parent's setup so inherited defaults run first; propagate
822+ # its False return so a parent default that raised still aborts
823+ # instance creation rather than being silently swallowed here.
824+ if parent_with_defaults is not None :
825+ decl = parent_with_defaults .method_decl (MYPYC_DEFAULTS_SETUP )
826+ parent_ok = builder .builder .call (decl , [self_var ], [ARG_POS ], [None ], cdef .line )
827+ fail_block , continue_block = BasicBlock (), BasicBlock ()
828+ builder .add (Branch (parent_ok , continue_block , fail_block , Branch .BOOL ))
829+ builder .activate_block (fail_block )
830+ builder .add (Return (builder .false ()))
831+ builder .activate_block (continue_block )
832+
812833 for stmt , origin_module in default_assignments :
813834 lvalue = stmt .lvalues [0 ]
814835 assert isinstance (lvalue , NameExpr ), lvalue
@@ -833,26 +854,6 @@ def generate_attr_defaults_init(
833854 builder .add (Return (builder .true ()))
834855
835856
836- def check_deletable_declaration (builder : IRBuilder , cl : ClassIR , line : int ) -> None :
837- for attr in cl .deletable :
838- if attr not in cl .attributes :
839- if not cl .has_attr (attr ):
840- builder .error (f'Attribute "{ attr } " not defined' , line )
841- continue
842- for base in cl .mro :
843- if attr in base .property_types :
844- builder .error (f'Cannot make property "{ attr } " deletable' , line )
845- break
846- else :
847- _ , base = cl .attr_details (attr )
848- builder .error (
849- ('Attribute "{}" not defined in "{}" ' + '(defined in "{}")' ).format (
850- attr , cl .name , base .name
851- ),
852- line ,
853- )
854-
855-
856857def create_ne_from_eq (builder : IRBuilder , cdef : ClassDef ) -> None :
857858 """Create a "__ne__" method from a "__eq__" method (if only latter exists)."""
858859 cls = builder .mapper .type_to_ir [cdef .info ]
0 commit comments