fix(tpc): 成员偏移以「实际 GEP 用的 struct 实例」为准,并报出双实例不一致

类别:a(编译器缺陷 —— 偏移与 GEP 不同源,整类字段偏移平移)

现场:TransPyV/App/lib/core/Gen/Class/ClassGenGeneric.vp:346
    return ast.Constant(pool, cnst.const_kind, cnst.int_val, cnst.float_val,
                        cnst.str_val, cnst.lineno, cnst.col_offset)
报「调用参数类型不匹配:指针类型 %"c772afbbfd495aca.MemManager"* 不能隐式转换为
整数参数 i32」,逐项列表显示实参 [6](= cnst.lineno)求值成 MemManager*。

取证(TransPyV/temp/5df907d3a02a0bac.stub.ll:21/:23):
  %"…Constant" = { [0]i8*(vtable) [1]AST*(parent) [2]MemManager*(pool)
                   [3]i32(lineno) [4]i32(col_offset) … }
  __new__(Constant*, MemManager*, i32, i64, double, i8*, i32, i32)
成员表扁平序为 [parent, pool, lineno, col_offset, …] ⇒ base=1 时 lineno 偏移 3
(正确),base=0 时偏移 2 = pool,正是报出的那个指针类型;而 col_offset 偏移
3 落到 lineno 仍是 i32,所以**只有 [6] 报错** —— 与现象逐位吻合。

根因:FuncGen._member_base 的布局取自**类键** self.structs[cls],而 GEP 用的是
**取值处** ObjVal.type.pointee 那一份实例。同名类双注册(或一份已从 stub 加载完整、
另一份仍 opaque)时两份布局不同 ⇒ 用一份算偏移、用另一份读,整类字段偏移平移。
_class_has_vtable 对跨模块 AST 类恒为 False(其 docstring 已记录该已知静默路径),
base 只能靠 delta + 类型校准救回,两实例不同源时即失守。

改动:
1. FuncGen._member_base(ClassName, StructHint=None):hint 为**同类**(短名相同)的
   struct 实例时,以它计算 delta / 首元素 vtable 判据 / 类型校准;当它与类键上的
   实例不是同一对象时,每类一次 vlog().warning 报出两侧名字与元素数(opaque 或
   元素数),使"双注册"这一事实不再静默。
2. FuncGen._get_member_offset(..., StructHint=None) → _BaseNow() 透传(base 仍现算,
   不缓存)。
3. HandlesExprAttr._handle_attr_name:把 ObjVal.type.pointee 作为 hint 传入。

不传 hint 时行为与改前逐字一致;其余偏移消费点(字段写入 / 构造初始化等)本轮未动。
This commit is contained in:
TermiNexus committed 2026-09-29 18:18:49 +08:00
1 parent 935a5b372b
commit 415916b3dd
2 files changed
+51 -5

No files matched your search

+11 -1
View File
@@ -708,7 +708,17 @@ class ExprAttrHandle(BaseHandle):
Gen.class_member_bitoffsets[ClassName][AttrName] = bo
return self._load_bitfield_member(ObjVal, ClassName, AttrName)
offset = Gen._get_member_offset(AttrName, ClassName)
# 偏移与 GEP 必须**同源**:把"实际将被索引的那个 struct 实例"传给偏移计算。
# 同一类键上可能存在两个实例(同名类 sha1 冲突双注册 / 一方仍 opaque),
# 用类键上那份算偏移、用 ObjVal 这份做 GEP ⇒ 整类字段偏移平移
# (现场:`Constant.lineno` 偏移 2 落到 `pool`(MemManager*) → 第 6 个实参被报
# "指针不能隐式转整数 i32")。_member_base 收到不同实例时会报出事实。
_StructHint: Any = None
if isinstance(ObjVal.type, ir.PointerType):
_ObjPointee: Any = ObjVal.type.pointee
if isinstance(_ObjPointee, ir.IdentifiedStructType):
_StructHint = _ObjPointee
offset = Gen._get_member_offset(AttrName, ClassName, _StructHint)
# 检查 offset 是否超出实际结构体
if isinstance(ObjVal.type, ir.PointerType):
actual_pointee: Any = ObjVal.type.pointee
+40 -4
View File
@@ -351,9 +351,17 @@ class FuncGenMixin:
_Cls = _hit
return _Cls
def _member_base(self, ClassName: str | None) -> int:
def _member_base(self, ClassName: str | None, StructHint: Any = None) -> int:
"""类 struct 的成员偏移基址(vtable 占位槽数)——**全仓唯一真源**。
`StructHint`(可选):调用方手里**实际要用于 GEP 的那个 struct 实例**
(通常就是 `ObjVal.type.pointee`)。同一类键上可能同时存在两个实例
(同名类 sha1 冲突的双注册,或一个已从 stub 加载完整、另一个仍 opaque),
而偏移基址必须与"实际 GEP 用的那份布局"一致 —— 用键上那份算、用另一份读,
会让整类字段偏移平移(现场:`Constant.lineno` 偏移 2 落到 `pool`(MemManager*),
实参第 6 项被报"指针不能隐式转整数 i32")。传 hint 时以 hint 为准并**报出**
两侧差异(每类一次),不传时行为与改前逐字一致。
`_get_member_offset` 与所有"自算 base 再 GEP 字段"的消费点(构造/初始化、
字段默认值、继承方法 this 调整等)都必须走本方法,禁止各自直查
`class_vtable`/`_cross_module_vtable_classes`——旧实现只认这两个装饰器集合,
@@ -393,7 +401,34 @@ class FuncGenMixin:
_vlog().warning(f"补齐类成员展平失败: {_e}", "Exception")
_ByInheritVtable: bool = self._class_has_vtable(_Cls)
base: int = 1 if _ByInheritVtable else 0
_StructBase: ir.IdentifiedStructType | None = self.structs.get(_Cls)
# 布局来源:优先调用方**实际 GEP 用的那个实例**(StructHint)——偏移必须与
# 真正被索引的那份 body 同源。只在 hint 不可用时才回落到类键上的实例。
_RegBase: ir.IdentifiedStructType | None = self.structs.get(_Cls)
_StructBase: ir.IdentifiedStructType | None = _RegBase
if isinstance(StructHint, ir.IdentifiedStructType):
# 仅接受**同类**的 hint:接收者静态类型是基类、实际指向派生类实例时,
# hint 的布局属于派生类,拿它配 `class_members[_Cls]` 会算错 delta。
_HintShort: str = self._extract_short_name(getattr(StructHint, 'name', '') or '')
if _HintShort == _Cls:
_StructBase = StructHint
if _RegBase is not None and _RegBase is not StructHint:
# 同一类键上两个 struct 实例:报出事实(每类一次),避免"用一份算、
# 用另一份读"的静默偏移平移(本函数此处即该平移的判定源)。
_Rep: set = getattr(self, '_MemberBaseInstMismatch', None)
if _Rep is None:
_Rep = set()
self._MemberBaseInstMismatch = _Rep
if _Cls not in _Rep:
_Rep.add(_Cls)
_RegN: str = (f"{len(_RegBase.elements)} 元素"
if _RegBase.elements is not None else 'opaque')
_HintN: str = (f"{len(StructHint.elements)} 元素"
if StructHint.elements is not None else 'opaque')
_vlog().warning(
f"[成员偏移] 类 '{_Cls}' 存在两个 struct 实例:类键上 "
f"{_RegBase.name}({_RegN}) vs 取值处 {StructHint.name}({_HintN})"
f";偏移基址已按取值处实例计算(两者布局不同会让字段偏移平移)",
"MemberBase")
if (isinstance(_StructBase, ir.IdentifiedStructType)
and _StructBase.elements is not None and len(_StructBase.elements) > 0):
_NumElems: int = len(_StructBase.elements)
@@ -445,7 +480,8 @@ class FuncGenMixin:
base = _BestB
return base
def _get_member_offset(self, field_name: str, ClassName: str | None = None) -> int | None:
def _get_member_offset(self, field_name: str, ClassName: str | None = None,
StructHint: Any = None) -> int | None:
# base 判定用未归一的原始类名:_member_base 内部自行做同一套归一
# (_NormalizeMemberClass),二者结果逐字一致(避免二次归一在
# "rawptr[X] 展开命中 sha1 前缀 struct 键"这一角落改变取值)。
@@ -500,7 +536,7 @@ class FuncGenMixin:
# 偏移整体差 1(ast.ExceptHandler.children 落到 end_col_offset(i32),
# len 分派拿到 i32)。
def _BaseNow() -> int:
return self._member_base(_MemberBaseKey)
return self._member_base(_MemberBaseKey, StructHint)
# 预热(与原行为逐字一致):先触发一次 EnsureClassMembersFlattened,
# 再决定下面的懒加载。
_BaseNow()