fix(HandlesImports): 解冻 struct 加载——缓存键缺失也删、索引陈旧定点再读、opaque 条目不再压住好 body

类别:a(编译器缺陷)。三处都会把"该类本 Gen 内始终 opaque"**永久固化**,
下游表现为 Repl.vp:802 `GenApi._g_gen.TypeGenCType.CTypedefNames` 报
"基座 pointee 未匹配 Gen.structs 中任何 struct(同短名候选=['TypeGenCType'],
全部未命中)"——而磁盘上 temp/<sha1>.stub.ll 明明有 41 槽 body。

1) `_TryLoadStructFromStub` 尾部缓存键删除判据:原只在"该键**有**实例且 opaque"时删;
   若索引里**有该类的条目**、而该键此刻根本没有实例(例如只落了 sha1 全名键、短名键
   尚未建立),这一轮的成功缓存被永久留下 ⇒ 之后每次同名查询都在入口直接 return。
   现改为"无可用 body(缺失或 opaque)且索引有条目"即删键,失败可重试。

2) `_entry_priority`:把 `is_opaque` 提到 `is_temp` 之前。原顺序让**陈旧的 output 占位
   条目压住 temp 里已有的好 body**(两侧都有 body 时仍保持 output > temp 原判据)。

3) 新增 `_ReReadStructBodyFromDisk`:`_build_stub_index` 只按"文件名集合"增量,假定
   stub `只增不改`;而 Phase 2 会**原地重写** output/<sha1>.stub.ll(译文生成点 :2194、
   依赖变化重组点 :1785-1790)⇒ 新 body 永不进索引。现在"选中的索引条目没有 body"
   这条失败路径上按磁盘当前内容**定点再读一次**(只读那一个文件,取不到即维持原状)。

静态核验:compileall 通过;全仓 symtable 未定义名扫描 0 条。
This commit is contained in:
TermiNexus committed 2026-09-30 15:44:10 +08:00
1 parent 393ffefcd5
commit 83453c68db
1 file changed
+104 -2
+104 -2
View File
@@ -180,10 +180,55 @@ def PrependInheritedBlock(Gen: object, ClassName: str, Block: list) -> bool:
_HeadNames: list = [nm for nm, _ in _Members[:len(_BlockNames)]]
if _HeadNames == _BlockNames:
return False
# 块已**整段**位于表的**尾部**(表头由上一行覆盖):定义侧 HandlesClassDef 的展平是
# **追加**,块会落在尾部 ⇒ 若本次仍前置,表里就多出一整份副本(现场 41→73 槽)。
# 判据与成因见 InheritedBlockAlreadyPresent 的说明。
if InheritedBlockAlreadyPresent(list(_Members), Block):
return False
getattr(Gen, 'class_members')[ClassName] = list(Block) + list(_Members)
return True
def InheritedBlockAlreadyPresent(Table: list, Block: list) -> bool:
"""祖先字段块 `Block` 是否已**整段**位于 `Table` 的**头部或尾部**(名字 + IR 类型逐项相同)。
为什么查头/尾两处而不是"表中任意位置":这两处恰好就是块被插入的两个方向 ——
导入侧 `PrependInheritedBlock` 前置(⇒ 头),定义侧 HandlesClassDef 展平追加
(⇒ 尾)。表中段出现的同序列与**合法的同名并存**(子类在自有段重复声明父类字段,
body 本来就有两个槽)不可区分,判成"已存在"会误删本该有的槽,故刻意不判——
真出现中段重复时仍由 StructGen 的重名条目告警兜住。
为什么需要独立判据:两侧判据此前不对称 —— 导入侧只比"表头是否等于块"(前置幂等),
定义侧**没有任何判据**(直接 `class_members + _flat`)⇒ "块已在尾部"时整块再追加
一次,表里多出 `len(Block)` 条重复条目,body 随之多出同名槽:
现场(2026-09-30,on-disk 实证):`output/<sha1>.stub.ll` 的
`750b8c10137d273e.TypeGenCType` body **73** 槽 = temp(Phase 1 声明)**41** 槽
+ 其 `[9:41]`(32 项,恰为追加前表的**尾部**)的原样副本;TypeGenCDef /
TypeGenErr 同为 +32,ClassGenMixin / ClassGenVTable / ClassGenEnum /
ClassGenDef 为 +8(两阶段布局不一致的类共 7 个,全在 Generator/ClassGen/
TypeGen 继承链上)。下游表现:`Repl.vp:802`
`GenApi._g_gen.TypeGenCType.CTypedefNames` 因两份 body(41/73)歧义而
"基座 pointee 未匹配 Gen.structs 中任何 struct"。
"""
_n: int = len(Block)
if _n == 0 or _n > len(Table):
return False
def _SameAt(_Start: int) -> bool:
for _j in range(_n):
_t: Any = Table[_Start + _j]
_b: Any = Block[_j]
if (not isinstance(_t, tuple) or not isinstance(_b, tuple)
or len(_t) < 2 or len(_b) < 2):
return False
if _t[0] != _b[0] or _t[1] != _b[1]:
return False
return True
return _SameAt(0) or _SameAt(len(Table) - _n)
class ImportHandle(BaseHandle):
_struct_Load_cache = {}
_pyi_cache = {}
@@ -2807,6 +2852,44 @@ class ImportHandle(BaseHandle):
return any(_k[0] == class_name for _k in _inprog)
return False
def _ReReadStructBodyFromDisk(self, full_name: str, file_sha1: str, from_output: bool) -> str | None:
"""按**磁盘当前内容**重取一个结构体的 body 文本(索引陈旧时的定点再读)。
为什么需要:`_build_stub_index` 只按"**文件名集合**"增量解析,假定 stub 文件
`只增不改`;而 Phase 2 会**原地重写** `output/<sha1>.stub.ll`(译文生成点
`Phase2Translator:2194`、"依赖变化重新组合"点 `:1785-1790`)⇒ 同名文件的新
body 永不进索引,索引里留的可能是先前那份 `= type opaque`(或更短的一份)
⇒ 该类在本 Gen 内被"缓存 + 索引"双重固化:现场 `Repl.vp:802`
`GenApi._g_gen.TypeGenCType.CTypedefNames` 报"基座 pointee 未匹配 Gen.structs
中任何 struct"(磁盘上 `temp/<sha1>.stub.ll` 明明有 41 槽 body)。
只在"选中的索引条目**没有 body**"这条失败路径上调用一次;读不到返回 None
(不构造、不兜底,交由调用侧维持原状并报根因)。
"""
try:
ProjectRoot: str = self._find_project_root()
except Exception:
return None
_dir: str = os.path.join(ProjectRoot, 'output' if from_output else 'temp')
_path: str = os.path.join(_dir, f"{file_sha1}.stub.ll")
try:
if not os.path.isfile(_path):
return None
with open(_path, 'r', encoding='utf-8') as _f:
_content: str = _f.read()
except Exception:
return None
# 先按 IR 全名精确取(含 sha1 前缀;`[`/`]` 等正则元字符用 re.escape 转义)
_m: Any = re.search(r'%"' + re.escape(full_name) + r'"\s*=\s*type\s*(.*)', _content)
if _m is not None:
return _m.group(1).strip()
# 再退化按**短名**取(文件内可能存的是不带 sha1 前缀的裸名)
_short: str = full_name.split('.', 1)[1] if '.' in full_name else full_name
_m2: Any = re.search(r'%"?([\w\[\]]+\.)?' + re.escape(_short) + r'"?\s*=\s*type\s*(.*)', _content)
if _m2 is not None:
return _m2.group(2).strip()
return None
def _TryLoadStructFromStub(self, class_name: str, Gen: LlvmGeneratorMixin, source_sha1: str | None = None) -> None:
# 缓存键包含 Gen 实例标识,确保每个模块的 Gen 都能独立加载 struct 定义,
# 同时仍能防止同一 Gen 内的递归重入。原先使用纯 class_name 作为键会导致
@@ -2850,10 +2933,22 @@ class ImportHandle(BaseHandle):
is_defining = 0 if type_sha1 == file_sha1 else 1
is_temp = 0 if from_output else 1
is_opaque = 0 if (struct_body and struct_body != 'opaque') else 1
return (sha1_match, is_defining, is_temp, is_opaque)
# 【is_opaque 前置于 is_temp(2026-09-30)】原顺序 (…, is_temp, is_opaque) 让
# **陈旧的 output 占位条目压住 temp 里已有的好 body**(现场:TypeGenCType
# output 条目在索引里是 opaque / temp 是 41 槽 —— 只因 output 排在前面就被选中,
# 该类在本 Gen 内永远 opaque ⇒ Repl.vp:802 的基座匹配失败)。有 body 的一侧
# 永远优先于"占位/陈旧"的一侧;两侧都有 body 时仍按 output > temp 的原判据。
return (sha1_match, is_defining, is_opaque, is_temp)
best_entry = min(entries, key=_entry_priority)
full_name, struct_body, file_sha1, from_output = best_entry
# 索引条目**没有 body**(空 / 'opaque')⇒ 可能是"索引陈旧":同名 stub 被**原地
# 重写**过(见 _ReReadStructBodyFromDisk 的成因说明)。此处按磁盘当前内容重取一次
# (只读这一个文件;取不到即维持原状,不兜底)。
if not struct_body or struct_body == 'opaque':
_fresh_body: str | None = self._ReReadStructBodyFromDisk(full_name, file_sha1, from_output)
if _fresh_body and _fresh_body != 'opaque':
struct_body = _fresh_body
# Bug 1 修复:从 full_name 提取 sha1 前缀(类型定义模块的 sha1),
# 而非使用 file_sha1(stub 文件所在模块的 sha1,可能只是导入方而非定义方)。
@@ -2917,8 +3012,15 @@ class ImportHandle(BaseHandle):
# 或 set_body 未生效),不保留成功缓存。保留会导致同 Gen 内后续调用永久
# 跳过补载,opaque 状态被固化,vtable 分派 GEP 直接崩溃。
# 失败状态可重试,待 stub 索引刷新 / 定义文件就绪后再加载。
# 【键缺失也算失败(2026-09-30)】原判据只在"**有**实例且 opaque"时删键;
# 若索引里**有该类的条目**、而该键此刻根本没有实例(`Gen.structs.get` 为 None,
# 例如只落了 sha1 全名键、短名键尚未建立),这一轮的成功缓存就被永久留下 ⇒
# 之后每次同名查询都在 :2855 直接 return,短名那份永远补不上 body
# (现场:Repl.vp:802 的基座实例从未匹配上,而唯一同短名候选一直是 opaque)。
_final_st = Gen.structs.get(class_name)
if isinstance(_final_st, ir.IdentifiedStructType) and _final_st.is_opaque:
_HasUsableBody: bool = (isinstance(_final_st, ir.IdentifiedStructType)
and not _final_st.is_opaque)
if not _HasUsableBody and entries:
try:
del self._struct_Load_cache[cache_key]
except KeyError: