mypyc 增量编译保留旧 vtable,补上跨模块祖先依赖
mypyc 分组增量编译可能漏记远端祖先模块,生成旧 vtable 引用。采用记录完整 MRO 依赖的版本,并连续验证祖先接口改变后的重编译。
mypyc 第一次编译成功,删除祖先类中的方法后再次增量编译,却在生成的 C 中报某个 CPyDef 成员不存在。源码里已经没有这个方法,生成文件仍然引用它,先查继承链中的模块有没有被记入增量依赖图。
一个具体触发条件是 separate=True:c.C 只显式继承 b.B,b.B 再继承 a.A。类型检查可以沿继承关系找到祖先,但 mypyc 生成的类布局和 vtable 还会引用远端祖先的方法;旧图没有记住 c 对 a 的这项依赖,a 接口改变后没有让 c 的生成 C 失效。
mypy 2.4.0 的正式代码包含修补:在类型检查完成、MRO 已可用时,mypyc 插件记录定义每个祖先类的模块为间接依赖。祖先接口变化才能到达仅声明子类的模块。
可以在一个独立验收目录建立最小三模块例子,不覆盖现有项目。分别保存为 a.py、b.py、c.py:
# a.py
class A:
def foo1(self) -> int:
return 1
def foo2(self) -> int:
return 2
# b.py
from a import A
class B(A):
def other(self) -> str:
return "b"
# c.py
from b import B
class C(B):
pass
同目录的 setup.py 使用分组编译:
from setuptools import setup
from mypyc.build import mypycify
setup(
name="mro-check",
ext_modules=mypycify(["a.py", "b.py", "c.py"], separate=True),
)
采用项目平台支持的编译器与 Python,在专用工具环境中选择版本后连续构建:
python -m pip install "mypy[mypyc]==2.4.0"
python setup.py build_ext --inplace
第一轮完成后,从 a.py 删除整个 foo2 方法,保留其他文件与缓存,再执行同一条构建命令。这一步才检查接口改变是否会触发必要重编译;每次先清理缓存,无法验出原图缺边。
第二轮应不再由 c 的旧 vtable 引用已经删除的 foo2 符号。随后在新 Python 进程检查保留的方法:
python -c "from c import C; assert C().foo1() == 1; assert C().other() == 'b'"
旧环境临时从干净缓存重新完整构建,可以帮助确认 stale 产物是否相关,但它不会补齐依赖图,下一次祖先改动仍可能重新暴露问题。也不要只手改生成的 C;生成器后续会覆盖它,接口布局还可能不一致。
原独立复现使用 Python 3.12.9 与基于 release-1.20 的 mypy fork,不能把这组环境当作更早 SQLGlot CI 的完整配置。修补针对分组编译与跨模块 MRO 失效跟踪;编译器缺失、真实源码错误或其他扩展 ABI 不兼容,仍需按各自堆栈处理。


