From 83453c68db77ae82fc71346da8c457ccbbebbfac Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Wed, 30 Sep 2026 15:44:10 +0800 Subject: [PATCH] =?UTF-8?q?fix(HandlesImports):=20=E8=A7=A3=E5=86=BB=20str?= =?UTF-8?q?uct=20=E5=8A=A0=E8=BD=BD=E2=80=94=E2=80=94=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E9=94=AE=E7=BC=BA=E5=A4=B1=E4=B9=9F=E5=88=A0=E3=80=81=E7=B4=A2?= =?UTF-8?q?=E5=BC=95=E9=99=88=E6=97=A7=E5=AE=9A=E7=82=B9=E5=86=8D=E8=AF=BB?= =?UTF-8?q?=E3=80=81opaque=20=E6=9D=A1=E7=9B=AE=E4=B8=8D=E5=86=8D=E5=8E=8B?= =?UTF-8?q?=E4=BD=8F=E5=A5=BD=20body?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 类别:a(编译器缺陷)。三处都会把"该类本 Gen 内始终 opaque"**永久固化**, 下游表现为 Repl.vp:802 `GenApi._g_gen.TypeGenCType.CTypedefNames` 报 "基座 pointee 未匹配 Gen.structs 中任何 struct(同短名候选=['TypeGenCType'], 全部未命中)"——而磁盘上 temp/.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/.stub.ll(译文生成点 :2194、 依赖变化重组点 :1785-1790)⇒ 新 body 永不进索引。现在"选中的索引条目没有 body" 这条失败路径上按磁盘当前内容**定点再读一次**(只读那一个文件,取不到即维持原状)。 静态核验:compileall 通过;全仓 symtable 未定义名扫描 0 条。 --- lib/core/Handles/HandlesImports.py | 106 ++++++++++++++++++++++++++++- 1 file changed, 104 insertions(+), 2 deletions(-) diff --git a/lib/core/Handles/HandlesImports.py b/lib/core/Handles/HandlesImports.py index c84ec3b..f53225d 100644 --- a/lib/core/Handles/HandlesImports.py +++ b/lib/core/Handles/HandlesImports.py @@ -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/.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/.stub.ll`(译文生成点 + `Phase2Translator:2194`、"依赖变化重新组合"点 `:1785-1790`)⇒ 同名文件的新 + body 永不进索引,索引里留的可能是先前那份 `= type opaque`(或更短的一份) + ⇒ 该类在本 Gen 内被"缓存 + 索引"双重固化:现场 `Repl.vp:802` + `GenApi._g_gen.TypeGenCType.CTypedefNames` 报"基座 pointee 未匹配 Gen.structs + 中任何 struct"(磁盘上 `temp/.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: