fix(HandlesClassDef): 类体字段与祖先字段重名 ⇒ fail-fast(消"继承段错位写错槽"与 split-brain)

类别:a 编译器缺陷(静默写错槽)+ c 源码违规(子类重复声明父类字段)的入口拦截。

判据(为什么重名必须终止,而不是容忍):
1. 子类同名声明会成为**自有字段**,与**同名继承项**在 class_members 里并存;跨模块侧
   的前置过滤器(HandlesImports._FlattenExt 前置段 :2516-2521)按"名字已在表内"过滤
   ——它本意只是防同一函数重入时重复前置,却把**同名继承项整条丢掉** ⇒ 继承段与
   struct body 的继承前缀失去一一对应:被丢项**之后**的继承字段表内下标整体前移 k,
   而查询侧回退路径用 `base + 表内下标`(FuncGen._get_member_offset)⇒ 落到类型完全
   不同的槽。现场:TypeGenCType 重复声明 5 个父类已有字段后,继承字段
   LastBuiltinsTypedefResult(下标 32)前移为 27 = CTypedefNameIdxBuilt(i32)
   ⇒ `self.Gen.TypeGenCType.LastBuiltinsTypedefResult = "t.CPtr"` 报
   "指针隐式转整数已禁止:不能把指针类型 i8* 赋给整数类型 i32"。
2. 父类方法按父类布局访问该名字的槽、子类方法按自有槽访问 ⇒ 同一变量 split-brain
   (一侧写、另一侧读),运行期静默错值。

实现:在类体自有字段收集段之后(:1047-1116)比对"自有字段名"与"沿 class_parents
传递收集到的全部祖先声明字段名",命中即 compile_error(含 文件:行:列、源码行、
祖先类名与逐字段明细,行号按 _line_offset 还原)+ sys.exit(1)。自有段用快照切分
(:903),祖先未就绪时视为无祖先 ⇒ 只少报不误报。

预检(避免新检查打断既有构建):用 TPC 自家解析器扫 App+includes 全部 .vp
(226 文件 / 382 类,口径=类体 AnnAssign + __init__ 带注解 self.x),既有代码
重名遮蔽数 = 0。

冒烟(合成样例,跑完删除):
  正例 class Child(Base) 重复声明 A ⇒ SystemExit code=1,报文给出 Child/A/Base/6:4;
  反例 Child 只声明新字段 C ⇒ 正常翻译。

配套:TransPyV 侧已撤销 TypeGenCType 的重复声明(0e5ebd0)。
This commit is contained in:
TermiNexus committed 2026-09-30 10:50:23 +08:00
1 parent 909d8ff0eb
commit 1903ed14f6
1 file changed
+77
+77
View File
@@ -5,6 +5,7 @@ if TYPE_CHECKING:
from lib.core.Translator.LlvmGenerator import LlvmGeneratorMixin
import vast as ast
import copy
import sys
import llvmlite.ir as ir
from lib.core.Exportable import EnumMember as ExportEnumMember
from lib.core.Handles.HandlesBase import BaseHandle, CTypeInfo, FuncMeta
@@ -897,6 +898,9 @@ class ClassHandle(BaseHandle):
Gen.class_requires[ClassName] = values
elif field_name == '__require_must__':
Gen.class_require_must[ClassName] = values
# 自有字段追加段的起点(供下方"字段名遮蔽"检查切分自有/继承两段;
# 此时 class_members[ClassName] 只含继承前缀,见 :825)。
_OwnFieldStart: int = len(Gen.class_members[ClassName])
for item in Node.body:
# 跳过编译期元数据字段(__provides__/__requires__/__require_must__)
if isinstance(item, ast.AnnAssign) and isinstance(item.target, ast.Name):
@@ -1040,6 +1044,79 @@ class ClassHandle(BaseHandle):
else:
Gen.class_member_signeds[ClassName][VarName] = None
Gen.class_member_bitfields[ClassName][VarName] = 0
# ============================================================
# 【字段名遮蔽 fail-fast】类体声明的字段名与**任一祖先声明的字段名**重名 ⇒ 终止编译
#
# 这不是风格问题:本仓两条既有机制决定了"同名"必然产出**写错槽**,且是静默的。
# ① 子类同名声明会成为**自有字段**,与**同名继承项**在 class_members 里并存;
# 跨模块侧的前置过滤器(HandlesImports._FlattenExt 前置段 :2516-2521)按
# "名字已在表内"过滤——它本意只是防同一函数重入时重复前置,却会把**同名继承项
# 整条丢掉** ⇒ 继承段与 struct body 的继承前缀失去一一对应:被丢项**之后**的
# 继承字段表内下标整体前移 k(k = 被丢项数),而查询侧回退路径用的是
# `base + 表内下标`(FuncGen._get_member_offset)⇒ 落到**类型完全不同的槽**。
# 现场:TypeGenCType 重复声明 5 个父类已有字段后,继承字段
# LastBuiltinsTypedefResult(下标 32)前移为 27 = CTypedefNameIdxBuilt(i32)
# ⇒ `self.Gen.TypeGenCType.LastBuiltinsTypedefResult = "t.CPtr"` 报
# "指针隐式转整数已禁止:不能把指针类型 i8* 赋给整数类型 i32"。
# ② 父类方法按**父类布局**访问该名字的槽、子类方法按**自有槽**访问 ⇒ 同名两槽
# split-brain(一侧写、另一侧读),运行期静默错值。
# 唯一合法写法:字段只在其**归属类**声明一次;子类要新字段就换名字。
# 合法例外(不在本检查范围):两个**互不为祖先**的类各自声明同名字段
# (如 CallGenBase / TypeGenBase 的 TempDir)——那不属于遮蔽。
# 判据来源:自有 = 本类类体追加段(:903 快照起);祖先 = 沿 class_parents 传递收集
# 各祖先 class_members 的声明名(含仅从 .vpi 加载的祖先;祖先未就绪则该项不成判据,
# 不产生误报)。
# ============================================================
_AncestorFieldNames: dict[str, str] = {}
_AncSeen: set[str] = set()
# class_parents 可能尚未建立(HandlesImports 同族代码也用 hasattr 兜底),
# 缺失时视为"无祖先"——只可能少报、不可能误报。
_ParentsMap: dict = getattr(Gen, 'class_parents', None) or {}
_AncStack: list[str] = list(_ParentsMap.get(ClassName) or [])
while _AncStack:
_Anc: str = _AncStack.pop()
if not _Anc or _Anc in _AncSeen:
continue
_AncSeen.add(_Anc)
for _An, _ in Gen.class_members.get(_Anc) or []:
if _An != '__vtable__':
_AncestorFieldNames.setdefault(_An, _Anc)
_AncStack.extend(_ParentsMap.get(_Anc) or [])
_OwnFieldNames: list[str] = [n for n, _ in Gen.class_members[ClassName][_OwnFieldStart:]]
_Shadowed: list[str] = sorted({_n for _n in _OwnFieldNames if _n in _AncestorFieldNames})
if _Shadowed:
_Decl: ast.AST | None = None
for _it in Node.body:
if (isinstance(_it, ast.AnnAssign) and isinstance(_it.target, ast.Name)
and _it.target.id in _Shadowed):
_Decl = _it
break
_src: str = getattr(Gen, '_current_source_file', '') or ''
_ln: int = (getattr(_Decl, 'lineno', 0) or getattr(Node, 'lineno', 0) or 0)
_col: int = getattr(_Decl, 'col_offset', -1) if _Decl is not None else -1
# 行号还原:AST 行号含 _inject_auto_imports 注入的 N 行(同
# HandlesBase._FailFastFixedArrayKeyword / _ErrorStack 各站点)。
_off: int = int(getattr(self.Trans, '_line_offset', 0) or 0)
if _off > 0 and _ln > _off:
_ln -= _off
_code: str = ''
try:
_code = (Gen._get_source_line(_ln) or '').strip()
except Exception:
_code = ''
_Pairs: str = '、'.join(f"'{_n}'(祖先 '{_AncestorFieldNames[_n]}' 已声明)"
for _n in _Shadowed)
_msg: str = (
f"类 '{ClassName}' 的类体字段与祖先类声明的字段**重名**:{_Pairs}。"
f"字段只能在其归属类声明一次(子类要新字段请改名;若确实要复用父类字段,"
f"删掉子类的这条声明即可)——重名会让该名字在 class_members 里变成"
f"「自有字段 + 同名继承项」两份,跨模块展平的按名过滤会丢掉继承项,"
f"使其后所有继承字段的表内下标整体前移、写入落到类型不同的槽"
f"(典型:指针值写进 i32 槽 → 「指针隐式转整数已禁止」);"
f"父类方法与子类方法还会各自访问不同槽(同一变量 split-brain)。"
f"位置 {_src}:{_ln}:{_col}")
_vlog().compile_error(_msg, _src, _ln, _code)
sys.exit(1)
# 字段收集完成,清除标志,允许后续 _generate_structs 正常设置 body
Gen._collecting_members_set.discard(ClassName)
existing_members: set[str] = {m[0] for m in Gen.class_members[ClassName]}