Skip to content

Commit 7a2ced7

Browse files
authored
[mypyc] Record MRO ancestor modules as deps of subclass-defining modules
[mypyc] Record MRO ancestor modules as deps of subclass-defining modules
2 parents 0898090 + d88e2be commit 7a2ced7

2 files changed

Lines changed: 138 additions & 9 deletions

File tree

mypy/build.py

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@
118118
MypyFile,
119119
OverloadedFuncDef,
120120
SymbolTable,
121+
TypeInfo,
121122
)
122123
from mypy.options import OPTIONS_AFFECTING_CACHE_NO_PLATFORM
123124
from mypy.partially_defined import PossiblyUndefinedVariableVisitor
@@ -3483,17 +3484,20 @@ def finish_passes(self) -> None:
34833484
if options.export_types:
34843485
manager.all_types.update(self.type_map())
34853486

3487+
# Possible sources of indirect dependencies:
3488+
# * Symbols not directly imported in this module but accessed via an attribute
3489+
# or via a re-export (vast majority of these recorded in semantic analysis).
3490+
# * For each expression type we need to record definitions of type components
3491+
# since "meaning" of the type may be updated when definitions are updated.
3492+
# * For mypyc-compiled modules only: modules defining MRO ancestors of
3493+
# classes defined here, since the generated C embeds each ancestor's
3494+
# method/attribute layout.
3495+
indirect_refs = self.tree.module_refs | self.type_checker().module_refs
3496+
if self.options.mypyc:
3497+
indirect_refs |= self.compiled_class_ancestor_refs()
34863498
# We should always patch indirect dependencies, even in full (non-incremental) builds,
34873499
# because the cache still may be written, and it must be correct.
3488-
self.patch_indirect_dependencies(
3489-
# Two possible sources of indirect dependencies:
3490-
# * Symbols not directly imported in this module but accessed via an attribute
3491-
# or via a re-export (vast majority of these recorded in semantic analysis).
3492-
# * For each expression type we need to record definitions of type components
3493-
# since "meaning" of the type may be updated when definitions are updated.
3494-
self.tree.module_refs | self.type_checker().module_refs,
3495-
set(self.type_map().values()),
3496-
)
3500+
self.patch_indirect_dependencies(indirect_refs, set(self.type_map().values()))
34973501

34983502
if self.options.dump_inference_stats:
34993503
dump_type_stats(
@@ -3535,6 +3539,27 @@ def patch_indirect_dependencies(self, module_refs: set[str], types: set[Type]) -
35353539
self.add_dependency(dep)
35363540
self.priorities[dep] = PRI_INDIRECT
35373541

3542+
def compiled_class_ancestor_refs(self) -> set[str]:
3543+
"""Modules defining MRO ancestors of classes defined in this module.
3544+
3545+
Only used for mypyc-compiled modules: the generated C for a class
3546+
(vtable arrays, getter/setter tables, object struct) references
3547+
every inherited method/attribute of every ancestor, including
3548+
ancestors defined in modules this module does not import directly.
3549+
Recording them as indirect dependencies makes an ancestor's
3550+
interface change re-trigger type checking (and hence C regeneration)
3551+
of this module. Only top-level classes are scanned: mypyc rejects
3552+
nested class definitions.
3553+
"""
3554+
assert self.tree is not None
3555+
mods: set[str] = set()
3556+
for sym in self.tree.names.values():
3557+
node = sym.node
3558+
if isinstance(node, TypeInfo) and node.module_name == self.id:
3559+
for ancestor in node.mro[1:]:
3560+
mods.add(ancestor.module_name)
3561+
return mods
3562+
35383563
def compute_fine_grained_deps(self) -> dict[str, set[str]]:
35393564
assert self.tree is not None
35403565
if self.id in ("builtins", "typing", "types", "sys", "_typeshed"):

mypyc/test-data/run-multimodule.test

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1832,6 +1832,110 @@ def _force_recompile() -> int:
18321832
from native import test
18331833
test()
18341834

1835+
[case testIncrementalCrossGroupInheritedMethodRemoved]
1836+
# Regression: under separate=True, a module whose only content is a subclass
1837+
# definition (`class Leaf(Mid): pass`) produces no expression types, so it
1838+
# recorded no dependency on the modules defining its transitive bases. When a
1839+
# Base method two hops up the inheritance chain was removed, staleness
1840+
# propagation stopped at other_mid (its own interface is unchanged) and
1841+
# other_leaf stayed fresh: its stale generated C still referenced the method
1842+
# in Leaf's vtable via exports_other_base.CPyDef_..., which no longer exists
1843+
# in the regenerated export table, and the build failed.
1844+
# compiled_class_ancestor_refs must record the MRO ancestors' modules as
1845+
# indirect deps so an ancestor's interface change recompiles the defining
1846+
# module too. (removed_method is declared after existing_method so the
1847+
# surviving call site's vtable slot is unaffected.)
1848+
import other_leaf
1849+
1850+
def test() -> int:
1851+
c = other_leaf.Leaf()
1852+
return c.existing_method(5)
1853+
1854+
[file other_base.py]
1855+
class Base:
1856+
def existing_method(self, x: int) -> int:
1857+
return x + 1
1858+
1859+
def removed_method(self, x: int) -> int:
1860+
return x + 100
1861+
1862+
[file other_base.py.2]
1863+
class Base:
1864+
def existing_method(self, x: int) -> int:
1865+
return x + 1
1866+
1867+
[file other_mid.py]
1868+
from other_base import Base
1869+
1870+
class Mid(Base):
1871+
def mid_method(self, x: int) -> int:
1872+
return x + 2
1873+
1874+
[file other_leaf.py]
1875+
from other_mid import Mid
1876+
1877+
class Leaf(Mid):
1878+
pass
1879+
1880+
[file driver.py]
1881+
from native import test
1882+
print(test())
1883+
1884+
[out]
1885+
6
1886+
[out2]
1887+
6
1888+
1889+
[case testIncrementalCrossGroupInheritedMethodRemovedNonEmptyLeaf]
1890+
# Same regression as testIncrementalCrossGroupInheritedMethodRemoved, but the
1891+
# subclass body is NOT empty: a class constant and a method that never uses
1892+
# self. Neither produces an expression whose type reaches Leaf's MRO, so the
1893+
# pre-existing dependency sources still record nothing for other_leaf, while
1894+
# its generated vtable references the ancestors regardless of body shape.
1895+
# Guards against narrowing compiled_class_ancestor_refs to pass-only bodies.
1896+
import other_leaf
1897+
1898+
def test() -> int:
1899+
return other_leaf.Leaf().existing_method(5)
1900+
1901+
[file other_base.py]
1902+
class Base:
1903+
def existing_method(self, x: int) -> int:
1904+
return x + 1
1905+
1906+
def removed_method(self, x: int) -> int:
1907+
return x + 100
1908+
1909+
[file other_base.py.2]
1910+
class Base:
1911+
def existing_method(self, x: int) -> int:
1912+
return x + 1
1913+
1914+
[file other_mid.py]
1915+
from other_base import Base
1916+
1917+
class Mid(Base):
1918+
def mid_method(self, x: int) -> int:
1919+
return x + 2
1920+
1921+
[file other_leaf.py]
1922+
from other_mid import Mid
1923+
1924+
class Leaf(Mid):
1925+
FLAG = True
1926+
1927+
def foo(self) -> str:
1928+
return "foo"
1929+
1930+
[file driver.py]
1931+
from native import test
1932+
print(test())
1933+
1934+
[out]
1935+
6
1936+
[out2]
1937+
6
1938+
18351939
[case testCrossModuleInheritedAttrDefaultsSameGroup]
18361940
# separate: [(["native.py"], "grp1"), (["other_a.py", "other_b.py"], "grp2")]
18371941
# Regression: with the subclass (other_a) and base (other_b) in the same

0 commit comments

Comments
 (0)