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: