From b06dac1d6b23abfa9d5c468d2135efc7d905e83f Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Tue, 29 Sep 2026 23:18:37 +0800 Subject: [PATCH] =?UTF-8?q?fix(LLVMCG):=20=E5=AD=97=E6=AE=B5=E5=81=8F?= =?UTF-8?q?=E7=A7=BB=E6=94=B9=E7=94=A8=E3=80=8C=E5=86=BB=E7=BB=93=E8=A1=A8?= =?UTF-8?q?=E6=9C=89=E6=95=88=E6=80=A7=E9=94=9A=E7=82=B9=20+=20=E5=B0=BE?= =?UTF-8?q?=E9=83=A8=E5=AF=B9=E9=BD=90=E3=80=8D=EF=BC=8CAnnAssign.value=20?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E8=AF=BB=E5=88=B0=20i32?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 类别: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 通过。 --- lib/core/LLVMCG/FuncGen.py | 56 +++++++++++++++++++++++++++++++++++- lib/core/LLVMCG/StructGen.py | 8 ++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/lib/core/LLVMCG/FuncGen.py b/lib/core/LLVMCG/FuncGen.py index e5d60e3..f1ebcc5 100644 --- a/lib/core/LLVMCG/FuncGen.py +++ b/lib/core/LLVMCG/FuncGen.py @@ -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() diff --git a/lib/core/LLVMCG/StructGen.py b/lib/core/LLVMCG/StructGen.py index f2d54d7..b745a4e 100644 --- a/lib/core/LLVMCG/StructGen.py +++ b/lib/core/LLVMCG/StructGen.py @@ -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)」的函数名所蕴含的全部**点分前缀**集合。