From 2e7bac1816fa5ab4f7f4f378ed25d274eb88a602 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Wed, 30 Sep 2026 18:41:24 +0800 Subject: [PATCH] =?UTF-8?q?fix(HandlesImports/HandlesClassDef):=20?= =?UTF-8?q?=E5=89=A5=E6=8E=89"=E7=A5=96=E5=85=88=E5=9D=97=E5=89=8D?= =?UTF-8?q?=E7=BC=80=E7=9A=84=E9=99=88=E6=97=A7=E5=8D=8A=E6=88=AA=E5=89=AF?= =?UTF-8?q?=E6=9C=AC"=EF=BC=8C=E6=B6=88=E9=99=A4=E6=88=90=E5=91=98?= =?UTF-8?q?=E8=A1=A8=E9=87=8D=E5=90=8D=E6=9D=A1=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 类别:a(编译器缺陷) 现场(用户构建日志): WARN[StructGen]: [结构体布局] 类 'ClassGenDef' 的成员表含**重名条目**: 表 50 项 / 去重 42 项,现有 body 42 槽恰等于「去重名数 + vtable」 判据(磁盘实证 + 结构推导,非猜测): · ClassGenDef 自身 **0** 个字段(全是方法),父类 ClassGenBase 恰 **42** 个字段; temp / output 两份 stub 的 ClassGenDef body 都是 **42** 槽,且槽序 = ClassGenBase 的字段声明序 ⇒ body 正确、**表多出 8 条**(50 = 42 + 8)。 · ClassGenBase 自己的 Phase-1 stub 就是 42 槽(不是 8)⇒ 多出的 8 条并非"父类 stub 短", 只能来自**内存里父类表尚未收集完就被展平**(写入了半截祖先块 Block[:k],k=8)—— 正是 PrependInheritedBlock 文档里写明的"已知残留"。 · 重名条目不是布局槽位(见 FuncGen._get_member_offset 的重名段),却让 _member_base 的 _Delta = 42 - 50 = -8 走进类型校准,并让回退路径的 base + 表内下标 整体偏斜(同族现场:MatchCase / TypeGenResolve 的偏移算错)。 修复(判据全部为正向事实,且与插入方向无关): · 新增 NormalizeAncestralCopies(Table, Block, InsertAtHead) -> (新表, 是否已含完整块): ① 完整块在**头**、尾部挂前缀副本 ⇒ 剥尾(副本是被后来的整块前置推过去的); ② 完整块在**尾**、头部挂前缀副本 ⇒ 剥头(副本先写入、整块随后追加); ③ 完整块尚未出现 ⇒ 剥插入边(仅当这些名字已另有正本,见下)。 剥掉的每一项在(表里已有的 / 随后插入的)完整块里同名同型 ⇒ **不丢布局信息**, 与 PrependInheritedBlock 性质①"不删块内条目"不冲突(剥的是副本)。 第一版曾"两端都剥",离线冒烟立刻暴露会把**正本**当副本剥掉([A,B,C,D,A,B] → [C,D,A,B],再前置整块反而多出 C,D 两份)⇒ 改为先定位完整块在哪一端、再剥另一端。 · TrimStalePrefixCopy 判据:插入边连续游程与 Block[:j] 逐项同 name 同 IR 类型, 1 ≤ j < len(Block),且这 j 个名字在表里**各另有一份**(计数 ≥ 2)——最后一条把 "子类自有字段恰好与祖先块前缀同名同型"的误判排除。 · 两个插入点(HandlesImports.PrependInheritedBlock 前置 / HandlesClassDef 展平追加) 统一走该函数,判据对称;真正剥除时各留一条 WARN(含条数与前后表长)。 · PrependInheritedBlock 原有两条幂等提前返回("表头等于块" / InheritedBlockAlreadyPresent) 合并为 _HasFull 一条(语义等价),并更新其"已知残留"段落。 · StructGen 的重名 WARN 追加**逐条明细**(名@表内下标):单看"表 50 / 去重 42"无法区分 "祖先块前缀的陈旧副本"与"两条收集路径各登记一次"(两种成因修法完全不同)。 核验:py_compile + compileall 全 lib 通过;离线冒烟(调用真实函数,8 组用例 × 两个插入方向): ①/② 均 6→4 正确剥除,"无重复 / 带自有字段 / 类型不同 / 无正本 / 空表"均不改动。 未放宽任何 fail-fast。 --- lib/core/Handles/HandlesClassDef.py | 24 +++++- lib/core/Handles/HandlesImports.py | 124 +++++++++++++++++++++++++--- lib/core/LLVMCG/StructGen.py | 15 +++- 3 files changed, 149 insertions(+), 14 deletions(-) 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