fix(LLVMCG): 字段偏移改用「冻结表有效性锚点 + 尾部对齐」,AnnAssign.value 不再读到 i32

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

现象:编译 TranslatorGenMixin.vp 时 fail-fast —— getelementptr(GEP) 的指针操作数不是
指针类型:操作数类型=i32、操作数名='value.9'、GEP 名='vtable_slot_AST'。即
`aa.value.kind()`(TranslatorGenMixin.vp:788)的接收者读成了整数:AnnAssign.value 落到
槽 3(lineno, i32),随后虚分派取 vtable 时把 i32 当指针用。

根因(两条独立判据,均写进代码注释与告警报文):
1) 冻结表陈旧:StructGen 建 body 时把「成员名→槽位」挂在 struct 实例上,而 body 之后会被
   「不完整即清除」(StructGen 预检 existing_st.elements = None)与 stub 加载
   (HandlesImports._TryLoadStructFromStub 的 set_body)清空后重设,重设路径不更新该表
   ⇒ 陈旧表按旧布局取槽(AnnAssign 在「自有 4 字段 + vtable」时冻结 {'value': 3},body
   被重设为 12 槽正确布局后仍取槽 3)。修:建表时同时记下 body 对象身份
   (_tpc_member_body,set_body 每次赋新 tuple),查询侧用 `is` 判同一性,失效即不再采用。
2) 未展平成员表:表只含自有字段(父类前缀未补齐)时,「base(0/1) + 表内下标」会把整类字段
   整体前移(target/annotation 落到指针槽 ⇒ 类型巧合、静默读错对象;value 落到 i32 槽 ⇒
   本次崩溃)。修:表短于 body 字段区时按**尾部对齐**取槽(展平规则 = 父类前缀 + 自有字段
   ⇒ 自有字段恒为 body 尾部 len(表) 项),并用 _MemberSlotAccepts 逐项类型比对自校验;
   表自洽(长度 == 字段区)时本路径不介入 ⇒ 对既有正确行为零变化;REnum 是 max-variant
   镜像布局,明确排除。命中时每类留一次告警,报出表长/字段区/取槽结果。

验证:8 例冒烟(冻结表新鲜;陈旧×展平表 / ×自有表;无冻结×自有表 / ×展平表;无 vtable;
整类逐字段 = 8/9/10/11;尾部类型不符时落回旧路径)全部通过;两文件 py_compile 通过。
This commit is contained in:
TermiNexus committed 2026-09-29 23:18:37 +08:00
1 parent 90d81df4f8
commit b06dac1d6b
2 files changed
+63 -1

No files matched your search

+55 -1
View File
@@ -576,13 +576,67 @@ class FuncGenMixin:
if isinstance(_OffStruct, ir.IdentifiedStructType):
_OffEls: Any = _OffStruct.elements
_Frozen: Any = getattr(_OffStruct, '_tpc_member_slots', None)
if isinstance(_Frozen, dict) and field_name in _Frozen:
# 冻结表**有效性**判定:表挂在 struct 实例上,而 body 会被"不完整即清除"
# (StructGen 预检 `existing_st.elements = None`)与 stub 加载
# (HandlesImports._TryLoadStructFromStub 的 set_body)**清空后重设**,
# 重设路径不更新本表 ⇒ 陈旧表把字段指到别的槽。现场:AnnAssign 在
# "自有 4 字段 + vtable"(5 槽:i8*,AST*,AST*,AST*,i32)时冻结
# {'value': 3},随后 body 被重设为 12 槽正确布局(value 在 10),
# 查询仍读槽 3(lineno, i32)⇒ `.kind()` 虚分派 GEP 的操作数是 i32。
# 判据 = 建表时记下的 **body 对象身份**(set_body 每次赋新 tuple):
# 与当前 elements 非同一对象即视为失效,落到按布局求槽的路径。
_FrozenFresh: bool = (isinstance(_Frozen, dict)
and getattr(_OffStruct, '_tpc_member_body', None) is _OffEls)
if _FrozenFresh and field_name in _Frozen:
_FrozenSlot: Any = _Frozen[field_name]
if isinstance(_FrozenSlot, int) and (not _OffEls or 0 <= _FrozenSlot < len(_OffEls)):
return _FrozenSlot
_OffMembers: list = self.class_members.get(_ResolvedClass) or []
_BitFields: Any = getattr(self, 'class_member_bitfields', None)
if _OffEls and _OffMembers and not (_BitFields and _BitFields.get(ClassName)):
# ---- 尾部对齐(布局真源之二):表**短于** body 字段区时 ----
# 展平规则是"父类前缀 + 自有字段"(EnsureClassMembersFlattened 前置前缀、
# StructGen 按展平表建 body)⇒ 自有字段恒为 body 尾部 len(表) 项。表比
# 字段区(len(elements) - vtable 前缀)短,即"父类前缀尚未补齐"(父类
# 未就绪/展平失败):此时 base(0/1) + 表内下标 会把整类字段整体前移
# (现场:AnnAssign 表仅 4 项自有字段 ⇒ 'value' 读槽 3,见上)。改为按
# 尾部求前缀数,并**逐项类型比对**自校验(判据与建 body 侧同一套
# _MemberSlotAccepts):表自洽(长度 == 字段区)时本路径不介入,
# 对既有正确行为零变化;REnum 是 max-variant 镜像布局,不适用。
_FieldArea: int = len(_OffEls) - _BaseNow()
if (len(_OffMembers) < _FieldArea
and not self._IsRenumLayoutClass(_ResolvedClass)):
_TailBase: int = len(_OffEls) - len(_OffMembers)
_TailOk: bool = _TailBase >= 0
_iT: int = 0
while _TailOk and _iT < len(_OffMembers):
_nmT, _mtT = _OffMembers[_iT]
if not self._MemberSlotAccepts(_mtT, _OffEls[_TailBase + _iT]):
_TailOk = False
_iT += 1
if _TailOk:
_TIdx: int = -1
for _i5, (_n5, _) in enumerate(_OffMembers):
if _n5 == field_name:
_TIdx = _i5
break
if _TIdx >= 0:
# 每类只留一次痕(大类的每个字段都会命中本路径)
_TailKey: str = _ResolvedClass
_TailRep: set = getattr(self, '_MemberTailAlignedReported', None)
if _TailRep is None:
_TailRep = set()
self._MemberTailAlignedReported = _TailRep
if _TailKey not in _TailRep:
_TailRep.add(_TailKey)
_vlog().warning(
f"[成员偏移] 类 '{_ResolvedClass}' 的成员表短于 struct 字段区"
f"(表 {len(_OffMembers)} 项 vs 字段区 {_FieldArea} 槽,"
f"body {len(_OffEls)} 槽)⇒ 按**尾部对齐**取槽:"
f"字段 '{field_name}' = {_TailBase + _TIdx}"
f"(前缀 {_TailBase} 项 = body 尾部之外的部分)",
"MemberBase")
return _TailBase + _TIdx
_Cursor: int = _BaseNow()
_ElemN: int = len(_OffEls)
_PairedNames: set = set()
+8
View File
@@ -777,6 +777,14 @@ class StructGenMixin:
_SlotIdx += 1
struct_type._tpc_member_slots = _SlotMap
struct_type._tpc_member_base = 1 if has_vtable else 0
# 有效性锚点:本表只对"建表这一刻的 body"成立。body 之后可能被
# 清空重设(本文件上方预检的 `existing_st.elements = None`、
# HandlesImports._TryLoadStructFromStub 的 set_body),而挂在
# 实例上的属性不会随之更新 ⇒ 陈旧表会把字段指到别的槽(现场:
# AnnAssign 自有 4 字段时冻结 {'value': 3},body 重设为 12 槽后
# 'value' 仍读槽 3 = lineno(i32))。记下建表时的 **body 对象**
# (set_body 每次赋新 tuple),查询侧用 `is` 判同一性,O(1) 且无猜测。
struct_type._tpc_member_body = struct_type.elements
def _create_Vtable_globals(self) -> None:
# 「本模块有定义(非 declare)」的函数名所蕴含的全部**点分前缀**集合。