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:
1 parent
393ffefcd5
commit
83453c68db
1 file changed
+104
-2
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user