fix(LLVMCG): 成员表重名/联合类型字段不再打断布局配对,ExceptHandler.name 取到槽 9

类别:a 编译器缺陷(TPC 侧)

现象:翻译 TransPyV/App/lib/core/Gen/AsyncGenMixin.vp 时 `self._add_local(trans, hd.name)`
(:667)的第 2 个实参求值失败并终止编译。基址诊断给出的事实链:类 ExceptHandler
(%"831ce86a2fd82c0..." 即 astaux.vp)成员表 10 项 =
['parent','pool','lineno','col_offset','end_lineno','end_col_offset','children','children','type','name']
('children' 重复一次),struct body 10 槽 =
[i8*, AST*, MemManager*, i32×4, list*, AST*(type), i8*(name)],字段 'name' 计算偏移 = 10
⇒ 越界(正确槽 = 9)。

判据与修法(两条,均已写进代码注释与告警报文):
1) 布局配对断链:成员表里 'children' 的声明类型是**联合类型折叠后的通用指针**
   (`list[rawptr[AST]] | t.CPtr` ⇒ i8*),而 body 槽是具体指针(%"...list[...]"*)⇒
   原配对只比 `is` / `str()`,在这一条断链,其后所有条目不再消费槽位 ⇒ 目标字段退化为
   "base + 表内下标"('name' ⇒ 1 + 9 = 10)。修:两侧都是指针时复用存储/建 body 侧同一
   判据 `_MemberSlotAccepts`(通用指针 i8* 放行、具体指针按被指类型),标量仍走严格比较
   ——不引入新的过配对。修后 type = 8、name = 9 ✓。
2) 重名条目使长度判据失真:`_expected_count_pre = len(class_members) + vtable` 把重复项
   也算一槽 ⇒ 误判"body 不完整"而用**带重复项的表**重建 body(本模块布局比其它模块多一
   槽 ⇒ 跨模块 ABI 撕裂)。新增判据:表有重名**且** body 槽数恰等于「去重名数 + vtable」
   ⇒ 认定 body 已是权威布局(来自定义模块 stub),只报事实、不重建。
   另在"未被 body 配对命中"的回退路径上,把"成员表含重名条目 + 表项数/去重项数 + 该
   返回值取自回退路径可能失准"作为事实告警报出(每类一次)。

验证:冒烟复刻本现场(表含重名 'children' 且其类型为联合 i8*;body 10 槽)——逐字段
1..9 全部命中(parent=1 … children=7、type=8、name=9),无重名的自洽表行为不变;
FuncGen / StructGen 两文件 py_compile 通过。
This commit is contained in:
TermiNexus committed 2026-09-29 23:48:52 +08:00
1 parent 8f1ec5b715
commit bd92dfdadf
2 files changed
+69 -2

No files matched your search

+42 -2
View File
@@ -650,7 +650,19 @@ class FuncGenMixin:
# 配对类型必须同口径,否则该类字段会被整条跳过。
_mtEff: Any = _mtP if _mtP is not None else ir.IntType(32)
_etP: Any = _OffEls[_Cursor]
if _mtEff is _etP or str(_mtEff) == str(_etP):
_PairOk: bool = (_mtEff is _etP or str(_mtEff) == str(_etP))
if (not _PairOk and isinstance(_mtEff, ir.PointerType)
and isinstance(_etP, ir.PointerType)):
# 联合类型字段在成员表里存成通用指针(如 AST.children =
# `list[rawptr[AST]] | t.CPtr` ⇒ i8*),而 body 槽是具体指针
# (`%"...list[...]"*`):严格串比较会在这条**断链**,其后的
# 所有条目都不再消费槽位(cursor 不动)⇒ 目标字段全部退化成
# "base + 表内下标"(现场:ExceptHandler.name 表内下标 9 ⇒
# 偏移 10,越界 fail-fast,而正确槽是 9)。判据与存储/建 body
# 侧同一套(_MemberSlotAccepts:两侧都是指针时按指针转换规则,
# i8* 通用指针放行);标量仍走严格串比较,不引入新的过配对。
_PairOk = self._MemberSlotAccepts(_mtEff, _etP)
if _PairOk:
_PairedNames.add(_nmP)
if _nmP == field_name:
# 与旧下标法不一致时留痕:这正是"成员表与 struct 布局
@@ -677,7 +689,35 @@ class FuncGenMixin:
return _Cursor
_Cursor += 1
if _ResolvedClass and _ResolvedClass in self.class_members:
for i, (name, _) in enumerate(self.class_members[_ResolvedClass]):
# 只有走到这里(布局配对未命中)才做一次重名检查:重名条目**不占布局槽**,
# 却会让 `base + 表内下标` 整体后移(现场:ExceptHandler 表 10 项、其中
# 'children' 出现两次 ⇒ 'name' 下标 9 ⇒ 偏移 10 越界)。此处把事实报出来
# (每类一次),并标注该返回值取自回退路径、可能失准。
_MemList: list = self.class_members[_ResolvedClass]
_SeenNm: set = set()
_DupNm: list[str] = []
for _nmC, _ in _MemList:
if _nmC in _SeenNm:
if _nmC not in _DupNm:
_DupNm.append(_nmC)
else:
_SeenNm.add(_nmC)
if _DupNm:
_DupRep: set = getattr(self, '_MemberDupNamesReported', None)
if _DupRep is None:
_DupRep = set()
self._MemberDupNamesReported = _DupRep
if _ResolvedClass not in _DupRep:
_DupRep.add(_ResolvedClass)
_vlog().warning(
f"[成员偏移] 类 '{_ResolvedClass}' 的成员表含**重名条目** "
f"{_DupNm!r}(表 {len(_MemList)} 项 / 去重 {len(_SeenNm)} 项)——"
f"重名项不是布局槽位,`base + 表内下标` 会因此整体后移;"
f"本次字段 '{field_name}' 走的是回退路径(未被 body 配对命中),"
f"返回值可能失准。重名来源通常是「同一字段被两条收集路径各登记一次"
f"(类内 AnnAssign 声明 + 方法体 self.x 赋值)」或继承前缀重复前置。",
"MemberBase")
for i, (name, _) in enumerate(_MemList):
if name == field_name:
return _BaseNow() + i
# 继承链回退:当字段不在当前类的 class_members 中时(常见于跨模块导入
+27
View File
@@ -614,6 +614,33 @@ class StructGenMixin:
if ClassName in self.class_packed and not existing_st.packed:
existing_st.packed = True
continue
# 成员表**重名**时长度判据不可信:重名条目不是布局槽位(body 由定义模块的
# stub 决定),却会让 `_expected_count_pre` 多算一项 ⇒ 误判"body 不完整"
# 而用带重复项的表重建 body(本模块布局比其它模块多一槽 ⇒ 跨模块 ABI 撕裂)。
# 现场:astaux.ExceptHandler 表 10 项含两个 'children',而定义模块 stub 的
# body 恰为 10 槽 = vtable + 9 个不同字段名。判据:表有重名**且** body 槽数
# 恰等于「去重名数 + vtable」⇒ body 已是权威布局,只报事实、不重建。
_uniq_names_pre: set[str] = {n for n, _ in _members_pre}
if (len(_uniq_names_pre) < len(_members_pre)
and len(existing_st.elements) == len(_uniq_names_pre) + (1 if _has_vtable_precheck else 0)
and _vtable_first_ok and not _has_t_placeholder_pre):
_DupRep: set = getattr(self, '_StructDupMembersReported', None)
if _DupRep is None:
_DupRep = set()
self._StructDupMembersReported = _DupRep
if ClassName not in _DupRep:
_DupRep.add(ClassName)
_vlog().warning(
f"[结构体布局] 类 '{ClassName}' 的成员表含**重名条目**:表 "
f"{len(_members_pre)} 项 / 去重 {len(_uniq_names_pre)} 项,"
f"现有 body {len(existing_st.elements)} 槽恰等于「去重名数 + vtable」"
f"⇒ 认定 body 为权威布局(来自定义模块 stub),**不按重复表重建**"
f"(重建会多出一槽、与其它模块布局不一致)。"
f"请核对成员表为何重复登记同一字段名。",
"StructGen")
if ClassName in self.class_packed and not existing_st.packed:
existing_st.packed = True
continue
# struct body 不完整,清除 elements 和 stub 缓存以便重新加载。
# 注意:本仓 llvmlite 版本的字段名是**公开属性 `elements`**
# (types.py: `self.elements = None`;`is_opaque` = `elements is None`;