fix(HandlesImports/HandlesClassDef): 剥掉"祖先块前缀的陈旧半截副本",消除成员表重名条目
类别: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。
This commit is contained in:
1 parent
c2f53852ee
commit
2e7bac1816
3 files changed
+149
-14
No files matched your search
@@ -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 一致),
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user