diff --git a/lib/core/Handles/HandlesClassDef.py b/lib/core/Handles/HandlesClassDef.py index 434e647..645c728 100644 --- a/lib/core/Handles/HandlesClassDef.py +++ b/lib/core/Handles/HandlesClassDef.py @@ -832,15 +832,33 @@ class ClassHandle(BaseHandle): # 下游即 Repl.vp:802 的"基座 pointee 未匹配任何 struct")。 # 判据(名字 + IR 类型逐项连续匹配)与成因见 # HandlesImports.InheritedBlockAlreadyPresent;只跳过重复、不改其余布局。 - from lib.core.Handles.HandlesImports import InheritedBlockAlreadyPresent as _BlkPresent - if _BlkPresent(list(Gen.class_members[ClassName]), _flat): + from lib.core.Handles.HandlesImports import NormalizeAncestralCopies as _NormAnc + # 【规范化(2026-09-30)】_NormAnc 同时完成两件事: + # ① "块已整段在头/尾 ⇒ 不再追加"(等价于原 InheritedBlockAlreadyPresent); + # ② 剥掉两端挂着的"祖先块前缀的**陈旧半截副本**"——此前某次展平写入的是 + # 半截祖先块(父类表当时只收到前 k 项),本次整块追加就让那 k 条留在表里 + # 成重复(现场:ClassGenDef 表 50 / 去重 42 / body 42 ⇒ 8 条陈旧副本)。 + # 判据与成因见 HandlesImports.NormalizeAncestralCopies。 + _Trimmed: list + _HasFull: bool + _Trimmed, _HasFull = _NormAnc(list(Gen.class_members[ClassName]), _flat, InsertAtHead=False) + _Dropped: int = len(Gen.class_members[ClassName]) - len(_Trimmed) + if _Dropped > 0: + _vlog().warning( + f"[成员展平] 类 '{ClassName}' 追加祖先块({len(_flat)} 项)前剥掉 " + f"{_Dropped} 条**陈旧半截副本**(重名条目不占布局槽位,却会让 " + f"base/下标回退失准):表 {len(Gen.class_members[ClassName])} → " + f"{len(_Trimmed)} 项", + "成员展平") + Gen.class_members[ClassName] = _Trimmed + if _HasFull: _vlog().warning( f"[成员展平] 类 '{ClassName}' 的祖先块({len(_flat)} 项)已存在于成员表" f"(表 {len(Gen.class_members[ClassName])} 项)⇒ 跳过重复追加" f"(重复会让 body 多出同名槽、与 Phase 1 声明布局分叉)", "成员展平") else: - Gen.class_members[ClassName] = Gen.class_members[ClassName] + _flat + Gen.class_members[ClassName] = _Trimmed + _flat # ============================================================ # 多继承方法合并:遍历所有父类(声明顺序)收集方法(短名去重,先声明父类优先), # vtable 槽/方法名用"实际定义类.方法"(如 B.get_b,与 TPV 一致), diff --git a/lib/core/Handles/HandlesImports.py b/lib/core/Handles/HandlesImports.py index f53225d..3fbb542 100644 --- a/lib/core/Handles/HandlesImports.py +++ b/lib/core/Handles/HandlesImports.py @@ -167,9 +167,11 @@ def PrependInheritedBlock(Gen: object, ClassName: str, Block: list) -> bool: 源码层面的这种重名已由 HandlesClassDef 的"字段名遮蔽 fail-fast"拦下, 此处只是不让它把布局判据带偏。 - 已知残留:若此前只前置过**部分**祖先块(父类表当时较短),本次会整块再前置一次, - 该前缀出现重复条目。调用点上方刚对父类做过 force 重载(块内容稳定),故该情形仅在 - "父类表被中途改写"时出现,且重复条目会在成员槽失败报文的"名序"里直接可见。 + 已知残留(**已在下方修复,2026-09-30**):若此前只前置过**部分**祖先块(父类表当时 + 较短),本次会整块再前置一次,该前缀出现重复条目。现于前置前调用 + `DropStaleAncestralCopies` 剥掉两端上的陈旧半截副本(判据:插入边上与 `Block` 前缀 + 逐项同名同型,且这些名字在表里**另有**一份正本)⇒ 不再残留。目标串接点与 + HandlesClassDef 的追加侧共用同一函数(两侧判据对称)。 返回 True 表示本次真的前置了。 """ @@ -177,13 +179,23 @@ def PrependInheritedBlock(Gen: object, ClassName: str, Block: list) -> bool: _BlockNames: list = [nm for nm, _ in Block] if not _BlockNames: return False - _HeadNames: list = [nm for nm, _ in _Members[:len(_BlockNames)]] - if _HeadNames == _BlockNames: - return False - # 块已**整段**位于表的**尾部**(表头由上一行覆盖):定义侧 HandlesClassDef 的展平是 - # **追加**,块会落在尾部 ⇒ 若本次仍前置,表里就多出一整份副本(现场 41→73 槽)。 - # 判据与成因见 InheritedBlockAlreadyPresent 的说明。 - if InheritedBlockAlreadyPresent(list(_Members), Block): + # 【规范化:剥掉"祖先块前缀的陈旧副本"(2026-09-30)】判据与成因见 + # NormalizeAncestralCopies —— 本函数文档"已知残留"一段的实测残留 + # (ClassGenDef 表 50 项 / 去重 42 项 / body 42 槽)在此消除。 + # 返回的 `_HasFull` 与原有的两条幂等判据("表头等于块" / InheritedBlockAlreadyPresent + # 的头尾整段判据)等价,故原两条提前 return 合并为这一条。 + _Trimmed: list + _HasFull: bool + _Trimmed, _HasFull = NormalizeAncestralCopies(list(_Members), Block, InsertAtHead=True) + if len(_Trimmed) != len(_Members): + _vlog().warning( + f"[成员展平] 类 '{ClassName}' 的祖先块({len(Block)} 项)插入前剥掉 " + f"{len(_Members) - len(_Trimmed)} 条**陈旧半截副本**(重名条目不占布局槽位," + f"却会让 base/下标回退失准):表 {len(_Members)} → {len(_Trimmed)} 项", + "成员展平") + getattr(Gen, 'class_members')[ClassName] = _Trimmed + _Members = _Trimmed + if _HasFull: return False getattr(Gen, 'class_members')[ClassName] = list(Block) + list(_Members) return True @@ -229,6 +241,98 @@ def InheritedBlockAlreadyPresent(Table: list, Block: list) -> bool: return _SameAt(0) or _SameAt(len(Table) - _n) +def _EqRun(Table: list, TableStart: int, Block: list, BlockStart: int, Count: int) -> bool: + """`Table[TableStart : TableStart+Count]` 与 `Block[BlockStart : BlockStart+Count]` + 是否**逐项同 name 同 IR 类型**。""" + _i: int + for _i in range(Count): + _t: Any = Table[TableStart + _i] + _b: Any = Block[BlockStart + _i] + 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 + + +def TrimStalePrefixCopy(Table: list, Block: list, AtHead: bool) -> list: + """剥掉 `Table` **指定端**上那段"祖先块 `Block` 前缀的陈旧副本"(返回新表)。 + + 只在 `AtHead` 指定的那一端找,且**不会**碰另一端 —— 因为另一端往往是**正本** + (完整块本身)。实测反例(本函数第一版的两端都剥):`[A,B,C,D]+[A,B]` 这种 + "正本在头、副本在尾"的表,两端都剥会把**头的正本**剥掉,得到 `[C,D,A,B]`, + 再把完整块前置就变成 `[A,B,C,D,C,D,A,B]` —— 副本反而更多。故判据必须**先定位 + 正本**(见 NormalizeAncestralCopies),再剥另一端。 + + 判据(全部为正向事实): + · 该端连续游程 `Table[:j]`(AtHead)或 `Table[len-j:]` 与 `Block[:j]` 逐项同名同型; + · `1 ≤ j < len(Block)`:整块本身不算"陈旧副本"; + · 这 j 个名字在 `Table` 里各自**另有**一份(计数 ≥ 2)—— 陈旧副本的特征就是 + "名字已有一份正本";子类合法的自有同名段只会出现一次 ⇒ 该条件把"自家字段 + 恰好与祖先块前缀同名同型"的误判排除。 + """ + if not Table or len(Block) < 2: + return Table + _j_max: int = min(len(Table), len(Block) - 1) + _counts: dict = {} + _it: Any + for _it in Table: + if isinstance(_it, tuple) and len(_it) >= 2: + _counts[_it[0]] = _counts.get(_it[0], 0) + 1 + _j: int + for _j in range(_j_max, 0, -1): + _start: int = 0 if AtHead else len(Table) - _j + if not _EqRun(Table, _start, Block, 0, _j): + continue + _all_dup: bool = True + _i: int + for _i in range(_j): + if _counts.get(Block[_i][0], 0) < 2: + _all_dup = False + break + if _all_dup: + return Table[_j:] if AtHead else Table[:len(Table) - _j] + return Table + + +def NormalizeAncestralCopies(Table: list, Block: list, InsertAtHead: bool) -> tuple[list, bool]: + """插入祖先块 `Block` 前把 `Table` 规范化成"完整块最多出现一次",返回 `(新表, 是否已含完整块)`。 + + 三种现场(都由"**半截祖先块**被写入 + 事后整块插入"造成;`PrependInheritedBlock` + 文档里的"已知残留"即此),一并处理: + + ① 完整块在**头**、尾部挂着前缀副本 ⇒ 剥**尾**(副本是被后来的整块前置推过去的); + ② 完整块在**尾**、头部挂着前缀副本 ⇒ 剥**头**(副本先写入,整块随后追加); + ③ 完整块尚未出现、且**插入边**上挂着前缀副本 ⇒ 剥插入边。③ 只在"这些名字在表里 + 已另有正本"时成立(判据见 TrimStalePrefixCopy):**首次**写入半截块时表里还没有 + 正本,此时不剥(无可判据),代价是该半截块会先留在表里,等下一次整块插入时由 + ①/② 剥掉 —— 即本函数对残留是**自愈**的(`PrependInheritedBlock` 会被多个加载 + 路径重复调用,定义侧也有重复收尾)。 + + 实测现场(用户构建日志 2026-09-30):`ClassGenDef` 表 **50** 项 / 去重 **42** 项, + 而 body 恰为 42 槽(= 父类 ClassGenBase 的 42 字段,ClassGenDef 自身 0 字段) + ⇒ 多出的 8 条正是祖先块前缀的陈旧副本。重名条目**不是布局槽位**(见 + `FuncGen._get_member_offset` 的重名段),却让 `FuncGen._member_base` 的 + `_Delta = 槽数 - 表长 = 42 - 50 = -8` 走进类型校准、并让回退路径的 + `base + 表内下标` 整体偏斜(同族现场:MatchCase / TypeGenResolve 的偏移算错)。 + + 剥掉的每一项在(已在表里的、或随后插入的)完整块里同名同型 ⇒ **不丢任何布局信息**, + 与 `PrependInheritedBlock` 性质①"不删块内条目"不冲突(被剥的是副本,正本仍在)。 + """ + _n: int = len(Block) + if _n == 0 or not Table or len(Table) < _n: + # 表尚不足以容纳完整块:只可能是现场③(或表为空/正常),按插入边剥 + return TrimStalePrefixCopy(Table, Block, AtHead=InsertAtHead), False + _HasHead: bool = _EqRun(Table, 0, Block, 0, _n) + _HasTail: bool = _EqRun(Table, len(Table) - _n, Block, 0, _n) + if _HasHead: + return TrimStalePrefixCopy(Table, Block, AtHead=False), True + if _HasTail: + return TrimStalePrefixCopy(Table, Block, AtHead=True), True + return TrimStalePrefixCopy(Table, Block, AtHead=InsertAtHead), False + + class ImportHandle(BaseHandle): _struct_Load_cache = {} _pyi_cache = {} diff --git a/lib/core/LLVMCG/StructGen.py b/lib/core/LLVMCG/StructGen.py index 82bca9a..e707830 100644 --- a/lib/core/LLVMCG/StructGen.py +++ b/lib/core/LLVMCG/StructGen.py @@ -631,13 +631,26 @@ class StructGenMixin: self._StructDupMembersReported = _DupRep if ClassName not in _DupRep: _DupRep.add(ClassName) + # 重名条目**逐条列出**(名 + 表内下标):单看"表 50 / 去重 42" + # 无法判断是哪 8 条、落在哪一端,而"祖先块前缀的陈旧副本" + # (见 HandlesImports.DropStaleAncestralCopies)与"两条收集路径 + # 各登记一次"两种成因的**修法完全不同** ⇒ 报文必须自带判据。 + _SeenIdx: dict = {} + _DupDetail: list = [] + _idxS: int + for _idxS, (_nmS, _) in enumerate(_members_pre): + if _nmS in _SeenIdx: + _DupDetail.append(f"{_nmS}@{_idxS}") + else: + _SeenIdx[_nmS] = _idxS _vlog().warning( f"[结构体布局] 类 '{ClassName}' 的成员表含**重名条目**:表 " f"{len(_members_pre)} 项 / 去重 {len(_uniq_names_pre)} 项," f"现有 body {len(existing_st.elements)} 槽恰等于「去重名数 + vtable」" f"⇒ 认定 body 为权威布局(来自定义模块 stub),**不按重复表重建**" f"(重建会多出一槽、与其它模块布局不一致)。" - f"请核对成员表为何重复登记同一字段名。", + f"重名条目(名@表内下标)={_DupDetail[:24]!r}" + f"(共 {len(_DupDetail)} 条)。请核对成员表为何重复登记同一字段名。", "StructGen") if ClassName in self.class_packed and not existing_st.packed: existing_st.packed = True