fix(tpc): 字段偏移改为「布局真源优先」,成员表漂移不再整体平移

类别 a(编译器缺陷):_get_member_offset 返回 `base + 成员表下标`,但 class_members
是**可变**表(继承展平会在父类就绪后前置前缀、跨模块可按需重载/重跑),而 struct
body 一旦 set_body 即**永久冻结** ⇒ 表与布局的序位一旦漂移,整类字段偏移整体平移。
现场(IR 实证):Converter 模块读 `Alias.name` 落在 struct 槽 9(= asname),而定义
模块 Alias.__init__ 写入槽 8;`import t` 无 as 子句 ⇒ asname=NULL ⇒ TransPyV StubGen
误报「import 别名 name 为空(_copy_str 返回 None)」并终止 includes/stdint.vp 的 vpi
生成。同族旧现场:ast.Constant.lineno 取到 pool 槽(已由成员偏移同源修复过)。

- StructGen._generate_structs:set_body 时把「成员名 → 槽位」映射冻结到 struct
  实例上(_tpc_member_slots / _tpc_member_base)。用**实例属性**而非全局表:同一
  类名可能存在不止一个 struct 实例,偏移必须与被 GEP 的那一份布局同源;仅当
  member_types 恰为 `[vtable?] + 全部成员` 时记录(位域类折叠为单一 storage 槽,
  其字段位置另由 class_member_bitoffsets 表达;stub 已置 body 的情形不记录)。
- FuncGen._get_member_offset:新增两条「只看布局」的路径,优先于原下标法——
  ① 冻结映射命中即返回;
  ② 用**类型**把成员表逐项配对到 elements(类型不匹配、重名的条目视为方法/占位/
     漂移项,不占槽位),配对中类型相等的槽位即字段真实槽位。
  表与布局自洽时两者给出与旧下标法逐字相同的结果;都覆盖不到才落回下标法(对既有
  正确行为零变化)。另在「配对结果 ≠ 下标法」时每类每字段告警一次,报出成员表项数
  与 struct 槽数,把漂移来源变成可见事实。

本地冒烟(三类,均通过):自洽表 8 / 漂移表(类型不匹配)8(旧下标法 9)/
全同型漂移且命中冻结映射 8。
This commit is contained in:
TermiNexus committed 2026-09-29 19:57:57 +08:00
1 parent d305d3369b
commit f28ff1b414
2 files changed
+89

No files matched your search

+67
View File
@@ -555,6 +555,73 @@ class FuncGenMixin:
# 依赖补齐(上面的懒加载)后再确认一次展平(幂等);偏移基址由 _BaseNow()
# 在真正 return 处现算,不在此缓存。
_BaseNow()
# ---- 布局真源优先(配对式偏移):偏移只认"运行时真正被 GEP 的那份 struct body" ----
# class_members 是**可变**表(继承展平会在父类就绪后前置前缀、跨模块可按需重载),
# 而 struct body 一旦 set_body 即**永久冻结** ⇒ "base + 成员表下标"会随表漂移
# 整体平移(现场:Converter 模块读 `Alias.name` 取到槽 9(= asname),
# `import t` 无 as 子句 ⇒ asname=NULL ⇒ vpi 生成误报"import 别名 name 为空";
# 同族旧现场:ast.Constant.lineno 取到 pool 槽)。改为两条"只看布局"的路径:
# ① 建 body 时冻结下来的"成员名 → 槽位"映射(StructGen._generate_structs 挂载);
# ② 用**类型**把成员表逐项配对到 elements:类型不匹配的条目视为方法/占位/漂移项,
# 不占槽位;配对中类型相等的那个槽位即该字段的真实槽位。
# 表与布局自洽时两条路径给出与旧下标法**逐字相同**的结果(body 本按该表建);
# 都覆盖不到时落回旧下标法(对既有正确行为零变化)。
if field_name:
_OffStruct: Any = self.structs.get(ClassName)
if isinstance(StructHint, ir.IdentifiedStructType) and ClassName:
# 与 _member_base 同口径:只接受**同类**的 hint(接收者静态类型可能是基类,
# 实际指向派生类实例,其布局不属于本类)。
if self._extract_short_name(getattr(StructHint, 'name', '') or '') == ClassName:
_OffStruct = StructHint
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:
_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)):
_Cursor: int = _BaseNow()
_ElemN: int = len(_OffEls)
_PairedNames: set = set()
for _nmP, _mtP in _OffMembers:
if _Cursor >= _ElemN:
break
# 重名条目不是布局槽位(表构造/重载产生的重复项)⇒ 不占槽位
if _nmP in _PairedNames:
continue
# None 成员在 body 里被物化为 i32(见 StructGen 建 body 分支),
# 配对类型必须同口径,否则该类字段会被整条跳过。
_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):
_PairedNames.add(_nmP)
if _nmP == field_name:
# 与旧下标法不一致时留痕:这正是"成员表与 struct 布局
# 不自洽"的现场事实(每类每字段一次,便于定位漂移来源)。
_OldIdx: int = -1
for _i3, (_n3, _) in enumerate(_OffMembers):
if _n3 == field_name:
_OldIdx = _i3
break
if _OldIdx >= 0 and _BaseNow() + _OldIdx != _Cursor:
_RepKey: tuple = (_ResolvedClass, field_name)
_RepSet: set = getattr(self, '_MemberSlotDriftReported', None)
if _RepSet is None:
_RepSet = set()
self._MemberSlotDriftReported = _RepSet
if _RepKey not in _RepSet:
_RepSet.add(_RepKey)
_vlog().warning(
f"[成员偏移] 类 '{_ResolvedClass}' 的成员表与 struct "
f"布局不自洽:字段 '{field_name}' 下标法={_BaseNow() + _OldIdx}"
f"(成员表 {len(_OffMembers)} 项) vs 布局配对比={_Cursor}"
f"(struct {_ElemN} 槽)——已采用布局配对结果",
"MemberBase")
return _Cursor
_Cursor += 1
if _ResolvedClass and _ResolvedClass in self.class_members:
for i, (name, _) in enumerate(self.class_members[_ResolvedClass]):
if name == field_name:
+22
View File
@@ -755,6 +755,28 @@ class StructGenMixin:
# 让后续 _TryLoadStructFromStub 能正确设置 body。
if member_types:
struct_type.set_body(*member_types)
# ---- 冻结"成员名 → 槽位下标"映射(布局真源,挂在 struct 实例上)----
# struct body 一旦 set_body 即**永久冻结**,而 class_members 是**可变**
# 表(继承展平会在父类就绪后前置前缀、跨模块可按需重载/重跑),
# 两者序位会漂移。而字段访问用的是 `_get_member_offset` 的
# `base + 成员表下标`,漂移即"整类字段偏移平移"——现场:
# Converter 模块读 `Alias.name` 取到槽 9(= asname),`import t`
# 无 as 子句 ⇒ asname=NULL ⇒ vpi 生成报"import 别名 name 为空"。
# 这里把"建 body 那一刻"的真实槽位表记到 struct 实例上
# (用实例属性而非全局表:同一类名可能存在不止一个 struct 实例,
# 偏移必须与被 GEP 的那一份同源)。仅当 member_types 恰为
# `[vtable?] + 全部成员` 时记录(位域类折叠成单一 storage 槽,
# 其字段下标另由 class_member_bitoffsets 表达,不在此记录)。
_ExpectN: int = (1 if has_vtable else 0) + len(all_members)
if len(member_types) == _ExpectN:
_SlotMap: dict[str, int] = {}
_SlotIdx: int = 1 if has_vtable else 0
for _mnS, _ in all_members:
if _mnS not in _SlotMap:
_SlotMap[_mnS] = _SlotIdx
_SlotIdx += 1
struct_type._tpc_member_slots = _SlotMap
struct_type._tpc_member_base = 1 if has_vtable else 0
def _create_Vtable_globals(self) -> None:
# 「本模块有定义(非 declare)」的函数名所蕴含的全部**点分前缀**集合。