Commit Graph
253 Commits
Author SHA1 Message Date
TermiNexus 83453c68db fix(HandlesImports): 解冻 struct 加载——缓存键缺失也删、索引陈旧定点再读、opaque 条目不再压住好 body
类别:a(编译器缺陷)。三处都会把"该类本 Gen 内始终 opaque"**永久固化**,
下游表现为 Repl.vp:802 `GenApi._g_gen.TypeGenCType.CTypedefNames` 报
"基座 pointee 未匹配 Gen.structs 中任何 struct(同短名候选=['TypeGenCType'],
全部未命中)"——而磁盘上 temp/<sha1>.stub.ll 明明有 41 槽 body。

1) `_TryLoadStructFromStub` 尾部缓存键删除判据:原只在"该键**有**实例且 opaque"时删;
   若索引里**有该类的条目**、而该键此刻根本没有实例(例如只落了 sha1 全名键、短名键
   尚未建立),这一轮的成功缓存被永久留下 ⇒ 之后每次同名查询都在入口直接 return。
   现改为"无可用 body(缺失或 opaque)且索引有条目"即删键,失败可重试。

2) `_entry_priority`:把 `is_opaque` 提到 `is_temp` 之前。原顺序让**陈旧的 output 占位
   条目压住 temp 里已有的好 body**(两侧都有 body 时仍保持 output > temp 原判据)。

3) 新增 `_ReReadStructBodyFromDisk`:`_build_stub_index` 只按"文件名集合"增量,假定
   stub `只增不改`;而 Phase 2 会**原地重写** output/<sha1>.stub.ll(译文生成点 :2194、
   依赖变化重组点 :1785-1790)⇒ 新 body 永不进索引。现在"选中的索引条目没有 body"
   这条失败路径上按磁盘当前内容**定点再读一次**(只读那一个文件,取不到即维持原状)。

静态核验:compileall 通过;全仓 symtable 未定义名扫描 0 条。
2026-09-30 15:44:10 +08:00
TermiNexus 393ffefcd5 fix(HandlesAssign): self 属性写入的基址收口统一为 _ConcretizeGepBase;收口失败不再静默丢写
类别:a(编译器缺陷)。

1) 收口统一:`self.字段 = v` 的基址此前走**内联**孪生体搜索,缺
   `_TryLoadStructFromStub` 一步 —— 类键那份仍 opaque 且 Gen.structs 里尚无已具化
   孪生体时(body 只在 stub 文件里待加载),内联版会一路走到 GEP 撞 opaque 守卫
   (现场:includes/ast/astaux.vp:165 `self.arg = _copy_str(pool, arg)`,基址 pointee
   是 opaque 的 %"8312ce86a2fd82c0.Keyword"*,而同短名 'Keyword' 键那份已具化 10 槽)。
   现改为调用读路径的**同一判据源** HandlesExprAttr._ConcretizeGepBase /
   _ConcretePointeeFor:① 类键那份 + stub 补 body → ② 同短名已具化孪生体 →
   ③ 都拿不到则原样返回、由守卫报根因。偏移与 GEP 同源(返回实例同时作为
   _get_member_offset 的 StructHint)。

2) 收口失败改 fail-fast:三条路都拿不到 elements 时,此前会落回
   `offset >= max_offset` 的「存回临时变量」分支 —— 该临时变量不在对象布局里,
   后续 `self.attr` 读路径按 GEP 取值,写入被**静默丢弃**。现直接 FailFastError,
   并报出三次尝试的结果(不兜底)。

静态核验:py_compile 通过;全仓 symtable 未定义名扫描 0 条;以 _GuardIrBuilder +
真实 _ConcretizeGepBase 复现现场实例形状(opaque '8312ce86a2fd82c0.Keyword' +
已具化 'fef783da4d24330d.Keyword'(10 槽))→ 解析成功、GEP 正常发出(索引 3);
未收口的裸 opaque 基址仍被守卫拦下。
2026-09-30 15:14:31 +08:00
TermiNexus 7ee6ebe6b0 fix(类型转换): int→i1 目标改为真值归一化 != 0,与 bool(x)/if 条件同判据
类别:a(编译器缺陷)。TPC 侧此前对 i1 目标一律 trunc(取最低位):运行时值 2 →
最低位 0 → false,而同一值在 `if 2:` / `bool(2)`(icmp ne 0)为真 ⇒ 条件上下文与
落存储/强转两处结论相反(静默语义分裂)。

- MemoryOps._CoerceIntToI1(新增,唯一判据源):非 i1 整数 → `icmp ne x, 0`。
- MemoryOps._coerce_value:窄化且目标位宽 == 1 时走 _CoerceIntToI1。
- HandlesAnnAssign._StoreWithCoerce:同上(self.字段 / obj.字段 落存储)。
- HandlesExprCall._HandleTypeCastLlvm:显式强转目标 i1 时同上。

与 TransPyV 侧同批修复(coerce_to_type / _translate_t_type_cast)对齐。

注:TPC 的 `bool` 注解仍是 i8(t.CBool.Size=8),故 `b: bool = 2` 仍按 i8 保位存储
(int(b)=2);本次只覆盖**真 i1 目标**,双端 bool 位宽差异见
SymbolUtils.py:1218-1223 已记录的全局语义分裂。
2026-09-30 15:14:31 +08:00
TermiNexus 3efe83f1ea fix(编译期名称解析): 修 StructGen 缺 _vlog 导入与 HandlesImports 的 node/Node 误用
两处均为 a(编译器缺陷),且都被 HandlesImports.py:1055 的
`except Exception -> WARN 忽略异常` 吞掉,表现为模块声明静默退化
(本次现场:VpiReader.vp 阶段 WARN + 结构体声明路径中断)。非源码违规、
非机制漏豁免。

1) lib/core/LLVMCG/StructGen.py
   - `_generate_structs` 的「成员表含重名条目、body 已是权威布局」提示调用
     `_vlog().warning(...)`,但本文件从未定义/导入 `_vlog` ⇒ NameError。
     该分支一旦命中(astaux.ExceptHandler 一类),`_generate_structs` 整体中断。
   - 补 `from lib.core.VLogger import get_logger as _vlog`,与同目录
     FuncGen.py / TypeConvert.py 同口径。

2) lib/core/Handles/HandlesImports.py:1317
   - `_EmitExternalGlobalDeclLlvm` 形参名为 `Node`,该 elif 分支误写成小写 `node`
     ⇒ 只要传入的是**非带注解**的跨模块全局量(`ast.Assign`)就恒抛 NameError,
     分支整体不可达:外部全局变量从未声明 external,消费侧表现为
     unknown global / 类型占位。
   - 判据改回 `isinstance(Node, ast.Assign)`。

静态核验:对 TransPyC 全部 .py 做 symtable 未定义名扫描,结果 0 条;
`python -m py_compile` 通过。
2026-09-30 14:55:40 +08:00
TermiNexus e5f044dd03 chore(诊断): AnnAssign RHS 为 接收者链.方法(...) 时补接收者基址诊断 + 最近一次属性读取失败原因
【b 机制漏豁免 —— 只加取证,不改任何判定/IR】现场:翻译
TransPyV/App/lib/core/Gen/Class/ClassGenEnum.vp:59
`ty: llvmlite.LLVMType* = self.Gen.TypeGenResolve.resolve_typedef_or_map(pool, at.attr,
imported_modules, from_imports)` 报"(Call 形态)编译期求值为空"。

已按代码**排除**出来的事实(写进注释,供后续不再重复排查):
  · 方法声明缺失会在 HandlesExprCall._HandleMethodCallLlvm 内 raise
    "Undefined method …",不会走到该 fail-fast;
  · 该函数在"方法已找到"之后不会返回 None,唯一的 None 出口是
    `ObjVal = HandleExprLlvm(Node.func.value)` 为假(L3335-3337)——
    即**接收者链 `self.Gen.TypeGenResolve` 自身求值为空**才是根因;
  · 但原报文的"基址诊断"只在 RHS 是 **Attribute** 时才打印(HandlesBase:784),
    Call 形态下接收者失败原因被丢弃 ⇒ 报错点与真因相隔数层。

修法(纯取证):HandlesBase 的通用 fail-fast 增 `elif Call(func=Attribute)` 分支,补三行:
  ① `被调者接收者诊断:` —— 复用 `_DescribeAttrBase(Gen, Node.func.value)`:接收者
     (如 `self.Gen`)的基座形态 / var_struct_class / alloca 类型 / 是否在 Gen.structs /
     class_members 项数与字段名单 / struct body 元素数 / 该字段计算偏移;
  ② `最近一次属性读取失败:` —— 回显 `Gen._LastAttrNone` 的 (类, 字段, 原因):Call 形态下
     无法用 (类, 字段) 与被报节点比对,故此处**无条件**回显(Attribute 形态仍保持
     "必须与被报节点一致"的原判据,避免旧记录误导);
  ③ `被调方法=… 已声明同名方法键(示前 4 / 共 N):` —— 区分"方法根本没声明"与
     "接收者读不出来"(后者才会命中本 fail-fast,但这一行让两者一眼可分)。

未验证:本提交只改诊断文本,行为不变;需再跑一次构建才能看到新增三行。
2026-09-30 14:45:55 +08:00
TermiNexus 00edd90c28 fix(Handles*): GEP 基址 opaque pointee 收口统一为单一判据源,补齐同族 12 处缺口
【a 编译器缺陷】7aa12263 修好 `_get_attr_ptr` 的 self 分支后,同族其余 GEP 点仍会在
"基址 pointee 是另一份 opaque 同名实例"时抛裸 TypeError(被 IR 边界守卫拦下,报文为
"GEP 的基址指向**未具化**的具名结构体")。根因是同一套收口逻辑被各点各写一份、且
**多数点根本没写**:真正需要的判据只有一条。

新增唯一判据源(HandlesExprAttr):
  · `_ConcretePointeeFor(Gen, ClassName, Pointee)`:取该类的**已具化实例** ——
    ① 类键那份先 `_TryLoadStructFromStub` 补 body;② 否则 `Gen._FindConcreteTwin`
    (同短名、槽数最多);③ 都取不到 ⇒ None(**不兜底**,交守卫 fail-fast 报同短名
    实例清单 + TPC 发射点)。
  · `_ConcretizeGepBase(Gen, Ptr, ClassName, Tag)`:pointee 非 opaque ⇒ 原样返回
    (对既有正确路径零变化);opaque ⇒ bitcast 到已具化实例并**同时返回该实例**,
    供 `_get_member_offset` 作 StructHint(偏移与 GEP 必须同源,与 a43513c9 同口径)。

接入点(全部改为调用上述判据源,逐点行为只在"原本必崩"的情形下变化):
  · HandlesExprAttr._get_attr_ptr — self / 变量 / 属性链 / i8* / 下标 / 强转基座 六条分支
    (self 分支已由 7aa12263 修,此处并入统一判据;下标分支此前**完全没有**收口);
  · HandlesAnnAssign._HandleAttributeStoreLlvm — self / 变量 / 属性链 三条分支;
  · HandlesBase._get_int_ptr — 属性分支;
  · HandlesExprBuiltin(len() 的两个接收者 GEP:`obj.len` 与 list 特化字段);
  · HandlesMatch — REnum 变体 match 的载荷 GEP(变体 struct 可能未 set_body)。

冒烟(临时脚本已删,鸭子类型 Gen + 真实方法):
  · `_get_attr_ptr` self 分支 ⇒ 基址 `%"fef783da4d24330d.Keyword"*`(经 cast_self_Keyword)PASS;
  · `_get_attr_ptr` i8* 分支 ⇒ 同上 PASS;
  · `HandlesAnnAssign` self 分支 ⇒ GEP 基址为已具化实例 PASS;
  · 回归:基址本就具化时必须**原样**(不得多出 bitcast)PASS。
另:`python -m compileall TransPyC/lib` 全绿。
2026-09-30 14:42:56 +08:00
TermiNexus 7aa12263f5 fix(HandlesExprAttr/BaseGen): _get_attr_ptr 的 self 分支补 opaque 基址收口(孪生体 bitcast)+ GEP 守卫报出 TPC 发射点
【a 编译器缺陷】现场:Phase2 翻译 includes/ast/astaux.vp:165
`self.arg = _copy_str(pool, arg)`,守卫报
"getelementptr(GEP) 的基址指向**未具化**的具名结构体:基址类型=%"8312ce86a2fd82c0.Keyword"*,
GEP 名='arg'",而同短名实例清单显示 'Keyword' 键上那份已具化(10 槽)。

根因:`_get_attr_ptr` 的 **self 分支**是全仓"self 字段取址"家族里唯一**没有**做 opaque
收口的一处 —— 同函数下方各分支(变量分支 / 属性链分支 / i8* 分支 / 强转基座分支)都先
`_TryLoadStructFromStub` 补 body、再 bitcast 到已具化实例;`HandlesAssign` 的 `self.X = v`
写路径与读路径 `_gep_and_Load_member` 也都有同一套孪生体搜索(`_FindConcreteTwin`)。
本分支直接 `gep(SelfPtr, …)`,typed pointer 下 llvmlite 必须在编译期从 pointee 算元素类型
⇒ 撞 opaque 即抛裸 TypeError(被 IR 边界守卫拦下)。

修法(与同函数其余分支同口径,不引入新判据):
  · 基址 pointee 为 opaque 时,先在类键(`Gen.structs[ClassName]`)上 `_TryLoadStructFromStub`
    补 body;类键那份仍不可用则退回 `Gen._FindConcreteTwin`(同短名、取槽数最多);
  · 命中即 bitcast 到该实例,并把该实例作为 `StructHint` 传给 `_get_member_offset`
    —— 偏移与 GEP 必须同源(与 a43513c9 的"hint 只接受同类"口径一致);
  · 顺带补 `offset is None ⇒ return None`(原实现会把 None 塞进 `ir.Constant(i32, None)`,
    属同一函数其余分支已有的判断)。

【a 编译器缺陷·取证】`RequirePointerOperand` 的 opaque 分支新增一行
「TPC 发射点(由内向外)」:取 Python 栈中位于本仓 `lib/`、且不在 BaseGen.py(守卫自身)
的最内 3 帧,报出 `文件:行(函数)`。理由:该守卫的报文只能指到**语句**,而同一语句里常有
多个候选 GEP 点(本例 astaux.vp:165 在读写两侧共有 4 个 self 字段 GEP 点),逐个读代码
无法判定是哪一个 —— 本次即靠该取证能力闭环(先按"唯一漏收口的分支"修正,下一次构建若
仍失败会直接指出真实发射点)。只在失败路径执行,热路径零开销。

冒烟(临时脚本已删):
  · `_get_attr_ptr` 复刻现场(self 槽持有 opaque `%8312…Keyword*`、'Keyword' 键那份 10 槽)
    ⇒ 输出 GEP 基址类型 `%"fef783da4d24330d.Keyword"*`(经 `cast_self_Keyword`),PASS;
  · 守卫取证:伪造 lib/ 下的发射帧 ⇒ 报文含
    `TPC 发射点(由内向外): core/Handles/_FakeProbe.py:2(_LibEmit)`,PASS。
2026-09-30 14:35:50 +08:00
TermiNexus 49930c56c7 fix(LLVMCG/HandlesExprAttr): REnum 变体字段按唯一变体键取槽 + 修 _PointeeMatchesStruct 的「同模块前缀」误判
【a 编译器缺陷】两处根因,同一现场(CallGenDispatch.vp:787
`len_ap: llvmlite.LLVMType* = len_val.Ty.Pointee`):

1) `_PointeeMatchesStruct` 只比 sha1 前缀 ⇒ 把**同模块的兄弟类**当成同一个类
   (`50741ad4b4b0934d.ParamNode` 与 `50741ad4b4b0934d.LLVMType` 前缀相同,同出
   includes/llvmlite/__types.vp)。结构体分支迭代 Gen.structs 时先命中 ParamNode
   (它在 __types.vp 里声明更早)⇒ `_get_member_offset('Pointee','ParamNode')`
   直接 fast-fail,报出与真因无关的类名。判据收紧为:**同一对象** 或 **IR 全名逐字相同**
   (前缀标识的是模块,不是类)。同短名不同前缀(同一类的另一模块实例)仍由调用点的
   `_UniqShortCN`(同短名候选唯一)放行,不受影响。

2) REnum 的变体字段在旧路径上无处可取:'Pointee' 只属变体键 'LLVMType_Ptr',
   而主类 'LLVMType' 的成员表为空(HandlesClassDef:716)⇒ 即使接收者判对类名也必 fail-fast。
   新增 `FuncGen._RenumVariantFieldOwner`,在 `_get_member_offset` 里当"主类不声明该字段"时改按
   变体键取偏移。判据(逐条可查):① 主类经 `_IsRenumLayoutClass`(唯一判据源)判为 REnum;
   ② 变体键固定 `<主类短名>_<变体名>` 且成员表**首项恒为 `__tag`**(唯一产源:
   HandlesClassDef._EmitREnumLlvm 与 HandlesImports 的 REnum 跨模块注册);
   ③ 恰有**一个**变体声明该字段(多个 ⇒ 不猜,照常 fail-fast);④ 变体 struct 已注册。
   槽位正确的依据:变体 struct 的 body 是**主 REnum 镜像** `{i32 __tag, <max_variant_payload>}`,
   变体成员表 `[__tag] + 载荷` 与主 REnum 槽位一一对应。

3) 载荷槽按**最大变体**宽度物化('Pointee' 槽是 i64),而字段本身声明为指针 ⇒
   新增 `HandlesExprAttr._RenumVariantPayloadType` / `_CoerceVariantPayload`,在
   `_gep_and_Load_member` 载入后按字段声明类型收口(int→ptr: inttoptr;ptr→ptr: bitcast;
   int→int: trunc/zext;不可转换原样返回,交下游照常 fail-fast)。非 REnum 变体键逐字不变。

冒烟(临时脚本已删):_PointeeMatchesStruct 5 例、_RenumVariantFieldOwner 7 例、
载荷类型与转换 6 例、_get_member_offset 钩子(绑定真实方法)5 例 —— 全部通过。
2026-09-30 12:46:37 +08:00
TermiNexus 64ddabbe17 feat(HandlesAnnAssign/HandlesExprAttr): 左值注解靶型提示(target-typing)——属性链读错类名时按注解类兜回
现场:CallGenDispatch.vp:787 `len_ap: llvmlite.LLVMType* = len_val.Ty.Pointee`
报 "[fast-fail] 属性 'Pointee' 在类 'ParamNode' 中找不到(class_members=2 个: ['Next','Ty'])"。
'Pointee' 的真实归属是 LLVMType 的 REnum 变体镜像字段(LLVMType.Ptr.Pointee,
includes/llvmlite/__types.vp:33-34);ParamNode 只有 Next/Ty。更隐蔽的是**两边都有 Ty**
(Value.Ty = rawptr[LLVMType]、ParamNode.Ty = 裸 rawptr = i8*)⇒ 读错类不会当场报错,
只静默把类型变成 i8*,再往下 `.Pointee` 才炸。

机制(与 TPV translate_value 的 target_ty「使用处真实站点种类」同思路):
  ① HandlesAnnAssign._EvalRhsWithTargetHint:注解赋值求值 RHS 前,把**注解指向的结构体类**
     作为 `Gen._TargetStructHint` 传给属性链解析;求值结束(含异常)复位,不跨语句泄漏。
     两个 RHS 求值点(_reg_values 提升路径 + 常规路径)都改走该入口。
  ② HandlesExprAttr._apply_target_struct_hint:**仅当两条件同时成立**才改判——
     Ⅰ 当前解析出的类(或解析不出)**不声明**该字段(此刻原本会 fail-fast);
     Ⅱ 靶型类**确实声明**该字段。
     任一不满足即原样返回(不猜、不篡改本来正确的链);改判时 `_vlog().warning` 留痕。
     接入点:char*(i8*)分支与单级 Name 分支(含"原键解析不出"的情形)。

验证:py_compile 通过;`_apply_target_struct_hint` 冒烟 7 例全部符合预期——
无提示原样 / 原类不声明且靶型声明⇒改判(本次现场)/ 原类声明该字段⇒不改判 /
原键即靶型⇒原样 / 原键解析不出且靶型声明⇒改判 / 靶型也不声明⇒原样 / 靶型类不存在⇒原样。
2026-09-30 12:28:39 +08:00
TermiNexus a1c43a7b3b chore(诊断): MemberLookupFailFast 补取证——字段所有者、类键/实例名、登记为该类的变量与实际 pointee
现场:CallGenDispatch.vp:787 `len_ap: llvmlite.LLVMType* = len_val.Ty.Pointee`
报 "[fast-fail] 属性 'Pointee' 在类 'ParamNode' 中找不到(class_members=2 个:
['Next','Ty'])"。'Pointee' 实际属 LLVMType 的 REnum 变体镜像字段(LLVMType.Ptr.Pointee),
而 ParamNode 只有 Next/Ty ⇒ 说明**读取时的接收者类名被判成了 ParamNode**
(`len_val` 的注解与 RHS 都写的是 `llvmlite.Value*`,Value.Ty 才是 rawptr[LLVMType];
ParamNode.Ty 是裸 rawptr = i8*)。

本轮只补取证、不改行为,让下一轮能一次定位"是谁把接收者记成了 ParamNode":
  ① 本类键 → struct 实例 IR 名(键/实例名不一致 ⇒ 同一类名两份实例);
  ② 其它**声明了该字段**的类键列表(唯一者即"本该用的类");
  ③ var_struct_class 中登记为**本类**的变量(封顶 6 个)及其实际 pointee IR 名
     —— 记录类名与实际 IR 类型不一致时,直接圈定记录点。

验证:py_compile 通过。
2026-09-30 12:26:05 +08:00
TermiNexus 43853bc455 fix(HandlesExprAttr): 基座 pointee 与 Gen.structs 的匹配补「同 sha1 前缀」与「同短名唯一候选」
现场(Repl.vp:802):`ct_names: list[str]* = GenApi._g_gen.TypeGenCType.CTypedefNames`
的基座 `.TypeGenCType` 取值后 pointee 为 `%"750b8c10137d273e.TypeGenCType"*`(= 撤销
6a287b9 后的新 sha1),而 Gen.structs(250 键)里没有该 IR 名的实例 ⇒ 旧判据只有
"身份相同 / IR 全名逐字相同" ⇒ 全部未命中 ⇒ 属性链求值为空 → 带注解赋值判定"无值可存"
而终止编译。(a18ca4da 的记账正是把"基座形态 / pointee / 键数"三项事实直接印了出来,
本轮不必再猜。)

改动(判据分级,强到弱;找不到仍留痕失败,不兜底造类型):
  ① 身份相同(原判据);
  ② IR 全名逐字相同(原判据,覆盖"同名第二份实例");
  ③ 同 sha1 前缀 —— IR 名形如 `<sha1>.<短名>`,前缀相等即"同一模块的同一类";
  ④ 同短名——**仅当候选唯一**时放行(调用点算 `_UniqShortCN`):两个及以上同名类无法
     判定是哪一个,宁可留痕失败也不选错布局(同名类冲突在本仓一律以 sha1 全名区分)。
同时把"未命中"记账扩为:列出分级判据、同短名候选清单,以及"候选 >1 会被主动拒绝"。

验证:py_compile 通过;`_PointeeMatchesStruct` 冒烟 6 例(身份 / 同名第二份实例 /
同短名不同 sha1 / 裸短名 / 不同类 / 非 Identified 一侧)全部符合预期。
2026-09-30 12:20:20 +08:00
TermiNexus 2aa55f36b9 fix(LLVMCG/Handles): GEP 基址 opaque 时按同短名找已具化孪生体并 bitcast;守卫报文补实例清单
现场:includes/ast/astaux.vp:165 `self.arg = _copy_str(pool, arg)` 的基址 pointee 是
opaque 的 `%"<sha1>.Keyword"*` —— 同一类名常有两份 struct 实例(短名 'Keyword' /
sha1 全名 '<sha1>.Keyword'),"函数签名里的 self 用哪一份"与"StructGen 具化的是哪一份"
并不保证一致 ⇒ GEP 无法在编译期算元素类型(llvmlite 抛裸 TypeError),
_GuardIrBuilder 的 opaque 守卫先行 fail-fast。

改动:
1) 新增 BaseGen._FindConcreteTwin:按**同短名**(IR 全名相同者优先)在 structs 里取一份
   已具化实例(元素最多者),与既有"同名实例取字段最多版本"(HandlesAssign:1156-1163)
   同一判据;返回 None 表示确无孪生体(真因 = "body 从未 set_body")。
2) 写入路径 HandlesAssign(`self.字段 = ...`):pointee opaque 时 bitcast 到孪生体,
   并把它同时用作 struct_type 与 `_get_member_offset` 的 hint ⇒ 偏移与 GEP 同源。
3) 读取路径 HandlesExprAttr._gep_and_Load_member 共用同一工具函数(读写两路同判据,
   避免"写修了读没修"再吃一轮)。
4) opaque-GEP 守卫报文补"同短名 struct 实例(key(IR 名)=槽数)"清单:清单含已具化项
   ⇒ 属"偏移与 GEP 未同源";全是 opaque ⇒ 属"body 从未 set_body"——一次报文即可判定是哪一侧。

找不到孪生体时**不兜底**:仍由守卫 fail-fast 并报出实例清单(本仓"不许静默兜底"约定)。

验证:py_compile 通过(BaseGen / HandlesAssign / HandlesExprAttr 三文件)。
2026-09-30 11:57:07 +08:00
TermiNexus 55cbf1d261 fix(HandlesImports): 继承前缀前置改为"按表头判幂等",不再按名字过滤整块(重名继承项不再被删)
类别:a 编译器缺陷(静默删掉布局条目,使继承段下标整体前移、写到类型不同的槽)。

问题(旧判据):`_EmitExternalClassDeclLlvm` 的前置段用
`nm2 not in {class_members[ClassName] 整表所有名字}` 过滤祖先块。该判据把
**与子类自有字段重名的继承项整条删掉**,于是继承段与 body 的继承前缀失去一一对应:
被删项**之后**的继承字段表内下标整体前移 k(k = 被删项数),而查询侧回退路径用
`base + 表内下标`(FuncGen._get_member_offset)⇒ 落到类型完全不同的槽。
现场(本轮):TypeGenCType 重复声明 5 个父类已有字段后,继承字段
LastBuiltinsTypedefResult 的下标 32→27,`= "t.CPtr"` 写进 i32 槽 ⇒
"指针隐式转整数已禁止:不能把指针类型 i8* 赋给整数类型 i32"。

新判据(抽成模块级 PrependInheritedBlock,可独立验证):
  ① 不删块内条目——块内每项都对应 body 的一个继承槽,删任一项都会整体前移;
  ② 幂等——只问"表头前 len(块) 项的名字序列是否等于祖先块的名字序列",相等即整块
     已前置 → 不再前置(保住 2026-08-10 修的重复字段问题:BasicBlock [Next, Next, …]);
  ③ 同名并存——子类自有字段与继承项同名时两者都留(定义端 body 同样是"继承前缀 +
     自有段",两侧槽数一致);源码层面的这种重名已由 HandlesClassDef 的"字段名遮蔽
     fail-fast"拦下(1903ed14)。

冒烟(纯逻辑,跑完删除):首次前置=块+自有;重复调用不再前置;**同名项仍全保留**
(对照旧实现会删掉块首项 ⇒ 下标前移);空块 no-op。py_compile 通过。

已知残留(写进函数文档):若此前只前置过部分祖先块(父类表当时较短),本次会整块再前置
一次 ⇒ 该前缀出现重复条目;调用点上方刚对父类 force 重载(块内容稳定),该情形仅在
"父类表被中途改写"时出现,且重复条目会在成员槽失败报文的"名序"里直接可见。
2026-09-30 11:18:07 +08:00
TermiNexus a43513c9a7 fix(FuncGen): 成员表与 body 等长时不再做类型对齐校准(base 恒为 0),修 TypeGenMixin 字段偏移平移
判据:_member_base 的 delta 长度启发式在 _Delta == 0 时给出 base=0(FuncGen.py:441),
而 base=2 **只可能**来自其后的"类型一致性校准"(:446-480,对 b∈{0,1,2} 逐项比类型取高分)。
但该校准用的是**严格类型相等**:声明侧的联合类型(如 `list[str] | t.CPtr`)可能被记成
折叠后的 i8*,而 body 槽仍是具体指针(`%list[str]*`)⇒ b=0 丢掉这些匹配,某个平移 b
却能靠指针槽互相撞分拿到更高分。
_Delta == 0(成员表项数 == body 槽数)本身就是"逐位对齐"(定义端 body 由成员表生成;
跨模块端 body(.stub.ll) 与 .vpi 同一声明顺序)⇒ base 必须为 0,此时不应再校准。

现场(本次):TypeGenMixin 41 成员 / 41 槽(@t.NoVTable,_Delta=0),base 被抬到 2 ⇒
Generator.vp:136 `SubTypeGen.Gen = self`(Gen 声明 `%"47382dc8277c9d28.Generator"*`、
正确槽为 0)写到槽 40(`%"6fbe68e8fca81223.TypeSpecArena"*`)⇒
"指针隐式转换已禁止(非基本类型(结构体/类/联合)):不能把指针类型 … 隐式转为 …"。
(该报文本身是上一提交 909d8ff0 补的"成员槽来源明细",这次正是靠它直接读出了
base/下标/声明槽类型/布局槽类型四项事实。)

附带:成员槽失败报文再补"名序"(_DescribeMemberSlot 打印 index:name 全表)——偏移算错时
第一现场就是"名序与 body 布局不同源",逐项列出即可直接判定是哪一侧漂移,不必再猜。

验证:py_compile 通过。_Delta != 0 的既有校准现场(ast.Constant 的 `cnst.lineno` 落到
pool 槽等)不受影响(本次只加前置条件,未改校准逻辑本身)。
2026-09-30 11:15:58 +08:00
TermiNexus de89fda73d fix(HandlesClassDef/DeclarationGenerator): 未声明字段不再静默兜底 i32,推不出类型即 fail-fast
类别:a 编译器缺陷(静默兜底推断)+ 隐患预防。

判据(为什么必须去掉 i32 兜底):成员类型是 struct 布局的唯一来源,静默把"没声明/
推不出"的字段当成 i32 会把错误推到下游——值域不匹配时表现为与真因无关的报文
(同族既有现场:`free_lists: uint64*` 被当成 i32 后,`self.free_lists = base`(i8*) 报
"指针隐式转整数已禁止")。项目约定:类型信息缺失立即终止(与同函数 `self.X: T` 分支
HandlesClassDef.py:1356-1386 已有 fail-fast 同口径)。

改动 1(定义端布局,权威侧)HandlesClassDef._scan_node_for_self_assign 的 Assign 分支:
  删除 `MemberType = ir.IntType(32)` 默认值,改为显式 `MemberType = None` + 白名单定型;
  推不出即 compile_error(含 文件:行:列、源码行、取值原文、建议写法,行号按 _line_offset
  还原)+ sys.exit(1)。
  采纳形态(逐条写进注释,必须与 stub 侧一致):
    整型/布尔字面量→i32;浮点字面量→double;字符串字面量→i8*;
    值为本方法带注解形参→该形参类型;白名单调用 float()/double()→double、char()→i8、
    int()/long()/short()→i32、`x.len()`→i32;其余(None 字面量/BinOp/任意调用…)→终止。

改动 2(stub 声明侧)DeclarationGenerator._CollectOwnMemberTypes 的 Assign 分支:
  删除 `InferredType = 'i32'` 默认值,改白名单 + 推不出即 raise `[FATAL] …拒绝静默写成
  i32`;同时纠正两处与定义端不一致的映射:浮点字面量 'float'(f32) → 'double'(f64),
  布尔字面量 'i8' → 'i32'(定义端 bool 走 int 分支),否则 stub 与定义端槽宽/类型分叉。

预检(确认不打断既有构建):用 TPC 自家解析器扫全工作区 .vp(678 类;口径 =
类体 AnnAssign + 任意位置 `self.X: T` + 祖先传递声明),"未声明字段 + 首次裸赋值"
实例数 = 0(曾疑似的 4 例 `self.Next = None` 均为继承字段:G4Node/Value 继承
GSListNode 已声明 Next)。

冒烟(合成样例,跑完删除):
  A `self.flag = None`(未声明、None 无类型信息)⇒ SystemExit(1) + 明确报文;
  B `self.n = 5`(可定型)⇒ 正常翻译;C `flag: int` 已声明后赋 None ⇒ 正常翻译。

已知残留(本次未动,已在代码注释里写明,避免下次盲修):stub 侧方法体内裸
`self.X = ...` 仍被 seen_member_names 过滤(该集合只含本类自有声明、不含继承字段);
若放行,继承字段(如 G4Node.Next)会被当成自有字段重复加槽 → 反而制造布局分叉。
要放行必须先在该处补"祖先字段名"判定。
2026-09-30 10:58:23 +08:00
TermiNexus 1903ed14f6 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)。
2026-09-30 10:50:23 +08:00
TermiNexus 909d8ff0eb fix(诊断): _get_node_info 还原自动注入 import 的行偏移;属性写入失败补成员槽来源明细
1) 行号还原(b 机制缺陷,实测可证):Phase2Translator._inject_auto_imports 在源码头
   注入 import _list/_dict/json,AST 节点行号整体下移 N
   (translator._line_offset = N),而 Gen._current_source_lines 是**磁盘**原文。
   BaseGen._get_node_info / _DescribeValueNode 此前直接用 AST 行号读源码 ⇒ 同一次
   fail-fast 里出现两套互相矛盾的行号:_ErrorStack 帧印磁盘行 1140/1141,
   _get_node_info 印 1142,并把源码显示成 1142 行的 `return "t.CPtr"`
   (真实错误行是 1141 的 `...LastBuiltinsTypedefResult = "t.CPtr"`,与被指语句无关)。
   新增 BaseGen._DiskLineno,判据与 HandlesBase._FailFastFixedArrayKeyword:687、
   HandlesAssign._RaiseGlobalAttrError:2051、LlvmGenerator/HandlesBody 的
   _ErrorStack 各站点逐字一致;标签行号(_current_node_info)与源码行一并还原。

2) 成员槽来源明细:HandlesAssign 的「对象.字段 = 值」写入路径(:1182)此前
   _StoreWithCoerce 失败时只报隐式转换禁令,不报目标槽来源,报文与被指语句、
   偏移对不上。现与 `self.x = ...` 路径(:897-905)同口径补 _DescribeMemberSlot:
   附上 class_members 声明侧(项数/base/表内下标/声明槽类型)与布局侧
   (struct name/元素数/elements[offset])两侧数据,可直接判定是声明侧退化
   还是布局侧退化(本次现场即"表内下标被同名继承项过滤移位"一类)。
2026-09-30 10:46:42 +08:00
TermiNexus a18ca4da3b fix(ExprAttr): 嵌套属性读的每个 silent return 都留痕(含内层失败原因链),Repl.vp:802 类错误不再只能看到"基座形态"
类别:a 编译器缺陷(可观测性补齐:报错点与真因相隔数层)

现象:TPC 编译 TransPyV/App/lib/core/Repl.vp:802
  `ct_names: list[str]* = GenApi._g_gen.TypeGenCType.CTypedefNames`
报「带注解赋值表达式 …(Attribute 形态)编译期求值为空(表达式未解析出目标、无值可存)…
赋值将被跳过」,报文里只有「基座形态=Attribute(.TypeGenCType←Attribute(._g_gen←Name(GenApi)))」,
看不出是三级链的哪一级失败、为什么失败。

判据:`_handle_attr_nested`(多级属性读)里有 7 处 `return None` 是**静默**的,而
`Gen._LastAttrNone`(`_NoteAttrNone` 记账,供 `_DescribeAttrBase` 回显"读取侧原因")只在
`_handle_attr_name`/`_gep_and_Load_member` 一侧登记 ⇒ 本函数的失败在报文里完全没有原因。
且嵌套读取时**内层**的记账会被外层覆盖,即使内层登记了也回显不出来。

修法(纯记账,不改任何判定/IR):
1. `_handle_attr_nested` 的每处静默 return 都调 `_NoteAttrNone` 记下**具体事实**:
   · 基座求值为空/非指针 → 附 type + **内层链的失败原因**(把 `_LastAttrNone` 里"字段名不同"
     的那条记录串进来,形成原因链);
   · 类名提示未命中 Gen.structs 键(报 structs 键数);
   · struct 实例类型异常;
   · body 仍 opaque 且无泛型特化成员;
   · **字段在类中取不到偏移**(报成员表项数 / body 槽数 —— 跨模块成员表缺失或漂移的关键判据);
   · 槽是 void 占位;
   · 结构体分支 pointee 未匹配任何类(报 structs 键数)。
2. `_DescribeAttrBase` 的回显键是 (字段名, 类名),本次登记与报错节点一致 ⇒ 下次同类报错将
   直接给出"读取侧原因=…",一轮定位到级与因。

验证:py_compile EXIT=0。真因证据(同一现场、另一侧):`TypeGenCType` 类体一个字段都没声明,
5 个字段全靠方法体裸 `self.x = ...`;其 .vpi(temp/05d515186a7c3668.vpi)里确实没有
CTypedefNames —— 跨模块成员表缺该字段即本次链断之处,已在 TransPyV 侧按"跨模块可见字段
必须在类体声明"补上(见另一仓 commit)。
2026-09-30 09:59:27 +08:00
TermiNexus c86ecb0c89 refactor(LLVMCG/Handles): 直建 ir.Function 点全部收敛到幂等入口,3 处不再吞 fail-fast
类别:a 编译器缺陷(同一根因的其它变体收口:函数声明创建的唯一入口 + fail-fast 不被吞)

背景(前几轮已修两处):llvmlite 的 `Function.__init__` 不是事务性的(名字在 `_set_name`
就进 module.scope,之后建 Argument/ReturnValue 才断言类型,最后才 add_global),中途失败会
留下悬空名字;本仓已加两道防线:全局守卫 `InstallFuncCreateGuard()`(回滚名字 + 带调用点的
FailFastError)与唯一幂等入口 `_CreateOrReuseFuncDecl`(创建前自检、同名同类型复用、异类型
带两侧签名 fail-fast)。但仍有 20+ 处**绕过入口直建**(同名时抛裸 DuplicatedNameError、且
名字被占/半注册时只能看到没有上下文的名字),以及若干处 `except Exception` 会把这颗
FailFastError 吞掉(声明静默缺失、真因丢失)。

改动:
1. 创建点收敛(21 处,行为等价或更严)——`X = ir.Function(<gen>.module, <fty>, name=<N>)`
   → `X = <gen>._CreateOrReuseFuncDecl(<N>, <fty>)`:
   · Handles:HandlesAnnAssign[llvm.memcpy]、HandlesAssign[llvm.memcpy.p0i8.p0i8.i64 ×2]、
     HandlesBase[GetOrCreateFuncDecl / GetOrCreateStubFuncDecl]、
     HandlesExpr[malloc]、HandlesExprBuiltin[strlen / llvm.memcpy]、
     HandlesExprCall[llvm.* 与普通 / win32 API / strlen / llvm.memcpy]、
     HandlesAsyncFunctions[malloc / create_* / resume_* / CRT 声明 / free]、
     HandlesExprLambda[lambda 包装]、HandlesFunctions[_replace_function_decl 的 old_func=None 分支]、
     HandlesImports[output stub 按需加载]
   · LLVMCG:BaseGen[_DeclareExternalFunc 与 6 处 --hotspot-sites 内部符号]、
     ExprGen[llvm.pow.f64]、VaArg[llvm.va_start / llvm.va_end]
   原有 globals/functions 预检查与随后的 `Gen.functions[...] = X` 赋值一律保留。
   注:`_DeclareExternalFunc` 收敛后外部声明会同时登记进 `self.functions`(幂等入口固有行为,
   与既有 `get_or_declare_c_func` 一致;该路径仅 --hotspot-sites 调试特性使用)。
2. fail-fast 不再被吞(3 处):在包裹创建的 `except Exception` 首部加
   `isinstance(_e, FailFastError) → raise`:
   · HandlesExpr[malloc 的 null 回退分支]、HandlesExprBuiltin[strlen]、HandlesExprCall[strlen]
   (其余异常维持原行为,本轮不扩大 fatal 面。)
   另有 HandlesFunctions / HandlesImports 的同类 handler 前几轮已具备 FFE 重抛,本次未重复改。

验证:13 个改动文件 `py_compile` 全部 EXIT=0;冒烟 4 例通过——同名同类型复用同一对象、
异签名 → FailFastError(报文含两侧签名 i64/i32)、名字被非函数全局占用 → FailFastError、
`llvm.va_start` 幂等。另:`ir.Function(` 在 lib 内仅剩 `BaseGen._CreateOrReuseFuncDecl` 内部
这一处(豁免点);`DecoratorPass.py` 的 `module` 为外部传入(非 Gen 的模块)故其两处保持原样。
2026-09-30 09:53:41 +08:00
TermiNexus 4d14334bd2 fix(LLVMCG): 成员表对齐改为「前缀锚 vs 尾部锚」按类型可写入性打分,修 Generator.vp:164 误报
类别:a 编译器缺陷(TPC 侧;修正 1776dfa6 / b06dac1d 的过宽判据)

现象:翻译 TransPyV/App/lib/core/Gen/Generator.vp 时 :164 `SubTypeGenErr.Gen = self` 报
「指针隐式转换已禁止:不能把 %"47382dc8277c9d28.Generator"* 隐式转为
%"6fbe68e8fca81223.TypeSpecArena"*」—— 目标类型是槽 41 的 Specs,而 Gen 的真身在槽 0。

判据:TypeGenErr 继承 TypeGenBase(41 槽)且只有 1 个自有字段 ⇒ body 42 槽;其成员表只含
['Gen'] 一项(**残缺前缀**,真身在 body 头部)。上一轮"尾部对齐"的前置校验(前缀数 == 父类
struct 槽数)在本例**巧合成立**:42 - 1 == 41 ⇒ 该表被锚到槽 41(Specs)⇒ 取到错误槽类型。
同族上一例为 Generator.vp:136(TypeGenMixin,已由 1776dfa6 挡住,但同类巧合仍会复现)。

修法:不再用"长度关系 / 父类槽数"去断言表的对齐方式(那是猜),改为**比较两个候选锚**的
逐项得分(判据与建 body / 存储侧同一套 `_MemberSlotAccepts`):
  · 前缀锚 `_BaseNow()` —— 展平表 / 残缺前缀;
  · 尾部锚 `len(body) - len(表)` —— 仅自有字段(父类前缀未补齐,真身在 body 尾部)。
只有尾部锚**严格更高分**才采用它;同分/更低一律保持原行为(配对 → 下标法)⇒ 对既有正确
布局零行为变化,同时消除"巧合成立"的假阳性。命中尾部锚时每类留一次告警(报出两锚得分)。

验证:5 例冒烟全部通过 ——
A TypeGenErr(声明类型分别为通用指针 / Generator*,body 42 槽).Gen → 0(修前 41);
C AnnAssign 仅自有字段表 → target/annotation/value/simple = 8/9/10/11(尾部锚仍生效);
D ExceptHandler 重名表 + 联合类型 children → name = 9(布局配对仍生效);
E 自洽展平表 .value → 10(行为不变);FuncGen.py py_compile 通过。
2026-09-30 00:39:06 +08:00
TermiNexus 1776dfa60a fix(LLVMCG): 尾部对齐加「前缀数 == 父类 struct 槽数」前置校验,修正 Generator.vp:136 误报
类别:a 编译器缺陷(TPC 侧;修正 b06dac1d 引入的过宽判据)

现象:翻译 TransPyV/App/lib/core/Gen/Generator.vp 时 `SubTypeGen.Gen = self`(:136)报
「指针隐式转换已禁止(非基本类型):不能把 %"47382dc8277c9d28.Generator"* 隐式转为
%"6fbe68e8fca81223.TypeSpecArena"*」。

判据(事实链):
· SubTypeGen 静态类型 = rawptr[TypeGenMixin.TypeGenMixin],Gen 字段声明在基类 TypeGenBase;
  该 struct 41 槽:槽 0 = Generator*(Gen),槽 40 = TypeSpecArena*(Specs)。
· 抛错点 `_StoreWithCoerce` 用的是 **GEP 出来的槽类型**(TargetType = Ptr.type.pointee)
  ⇒ 报错里的 TypeSpecArena* 说明 "Gen 的偏移被算成了 40"(Specs 那一槽),而不是 0。
· 成因:b06dac1d 的「尾部对齐」在"成员表短于 body 字段区"时按 `len(elements) - len(表)`
  求前缀数;TypeGenMixin 的表只有 ['Gen'] 一项(且其声明类型已退化成通用指针 ⇒
  类型自校验被放行),于是被锚到 body 末尾 = 槽 40。

修法:尾部对齐加**必需的前缀校验**——前缀数必须等于**父类 struct 的槽数**(展平前缀就是
父类整份布局,含其继承链);父类 struct 不可得(跨模块未加载 / opaque)时**不介入**。
TypeGenMixin:40 ≠ len(TypeGenBase)=41 ⇒ 不介入,落回下标法得 0(Generator*,赋值合法)。
(正面场景仍生效:AnnAssign 表仅 4 项自有字段、body 12 槽、父类 Stmt 8 槽 ⇒ 8 == 8。)

验证:冒烟两例——反例 TypeGenMixin.Gen → 0(修前 40);正例 AnnAssign 仅自有字段表仍得
target/annotation/value/simple = 8/9/10/11;FuncGen py_compile 通过。
2026-09-29 23:58:09 +08:00
TermiNexus bd92dfdadf fix(LLVMCG): 成员表重名/联合类型字段不再打断布局配对,ExceptHandler.name 取到槽 9
类别:a 编译器缺陷(TPC 侧)

现象:翻译 TransPyV/App/lib/core/Gen/AsyncGenMixin.vp 时 `self._add_local(trans, hd.name)`
(:667)的第 2 个实参求值失败并终止编译。基址诊断给出的事实链:类 ExceptHandler
(%"831ce86a2fd82c0..." 即 astaux.vp)成员表 10 项 =
['parent','pool','lineno','col_offset','end_lineno','end_col_offset','children','children','type','name']
('children' 重复一次),struct body 10 槽 =
[i8*, AST*, MemManager*, i32×4, list*, AST*(type), i8*(name)],字段 'name' 计算偏移 = 10
⇒ 越界(正确槽 = 9)。

判据与修法(两条,均已写进代码注释与告警报文):
1) 布局配对断链:成员表里 'children' 的声明类型是**联合类型折叠后的通用指针**
   (`list[rawptr[AST]] | t.CPtr` ⇒ i8*),而 body 槽是具体指针(%"...list[...]"*)⇒
   原配对只比 `is` / `str()`,在这一条断链,其后所有条目不再消费槽位 ⇒ 目标字段退化为
   "base + 表内下标"('name' ⇒ 1 + 9 = 10)。修:两侧都是指针时复用存储/建 body 侧同一
   判据 `_MemberSlotAccepts`(通用指针 i8* 放行、具体指针按被指类型),标量仍走严格比较
   ——不引入新的过配对。修后 type = 8、name = 9 ✓。
2) 重名条目使长度判据失真:`_expected_count_pre = len(class_members) + vtable` 把重复项
   也算一槽 ⇒ 误判"body 不完整"而用**带重复项的表**重建 body(本模块布局比其它模块多一
   槽 ⇒ 跨模块 ABI 撕裂)。新增判据:表有重名**且** body 槽数恰等于「去重名数 + vtable」
   ⇒ 认定 body 已是权威布局(来自定义模块 stub),只报事实、不重建。
   另在"未被 body 配对命中"的回退路径上,把"成员表含重名条目 + 表项数/去重项数 + 该
   返回值取自回退路径可能失准"作为事实告警报出(每类一次)。

验证:冒烟复刻本现场(表含重名 'children' 且其类型为联合 i8*;body 10 槽)——逐字段
1..9 全部命中(parent=1 … children=7、type=8、name=9),无重名的自洽表行为不变;
FuncGen / StructGen 两文件 py_compile 通过。
2026-09-29 23:48:52 +08:00
TermiNexus 8f1ec5b715 fix(LLVMCG): llvmlite Function 创建事务化 + 不再吞 fail-fast,悬空名字不再复现
类别:a 编译器缺陷(TPC 侧)

现象:编译 StubMergerIR.vp 时 `_EmitExternalFuncDeclLlvm` 报「函数声明名字悬空占用:
'50741ad4b4b0934d.LLVMTypeCore.__init__' 已进 module.scope,但 module.globals 中无对应
对象」,且该 FailFastError 被 `_LoadModuleDeclarationsFromFile` 的节点级 `except Exception`
吞成一行 WARN「忽略节点 ClassDef LLVMTypeCore 异常」后 continue。

机制(判据已写进代码注释与报文字段):
1) llvmlite 的 `Function.__init__` 不是事务性的:名字在 `_set_name` 即进 module.scope,
   之后逐个建 Argument/ReturnValue 才做类型断言,最后才 add_global;中间任一步抛错都不
   回滚 scope ⇒ 留下「globals 查无、scope 已占用」的悬空名字,后续同名创建只能报裸
   DuplicatedNameError(或本仓的「名字悬空占用」),而真因——上一处被吞的创建失败——已丢失。
   修法:BaseGen 新增 `InstallFuncCreateGuard()`(随模块导入装一次),在**失败路径**上
   回滚名字(补 llvmlite 缺的那半截事务,模块状态可重试)并抛 FailFastError,报文含
   函数名 / 本次签名 / 原始异常 / 创建调用点(栈上首个非 llvmlite、非包装函数的帧);
   `DuplicatedNameError` 原样上抛——那是各调用点有意处理的正常分支(如「已存在则复用」)。
2) 首建失败与该 FailFastError 被 5 处兜底吞掉,使真因退化成一行 WARN:
   · HandlesImports._LoadModuleDeclarationsFromFile 节点级 except(本现场的直接来源)
   · HandlesImports stub 声明加载循环的两层 except
   · HandlesImports._TryLoadFuncDeclsFromOutputStub 的裸 `except Exception: pass`
   · HandlesFunctions._replace_function_decl 的 `except Exception: return old_func`
     (此处 old_func 必为 None ⇒ 调用点把 None 当函数声明用)
   修法:一律 `except FailFastError: raise`;其余异常维持原有口径(严格模式上抛 / 其余告警)。

验证:4 例冒烟(非法签名 → FailFastError 且报文含调用点;名字已回滚 ⇒ 同名可重试成功;
重复创建仍是 DuplicatedNameError 且旧名未被回滚;合法创建零副作用)全部通过;
BaseGen / HandlesImports / HandlesFunctions 三文件 py_compile 通过。
2026-09-29 23:31:57 +08:00
TermiNexus b06dac1d6b 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 通过。
2026-09-29 23:18:37 +08:00
TermiNexus 90d81df4f8 fix(tpc): 自指针指向 opaque 同名实例时归一为具化实例;GEP 边界拦下 opaque pointee
类别 a(编译器缺陷):llvmlite 的 typed pointer GEP 必须在编译期算出元素类型——
`GEPInstr` → `ptr.type.gep(i)` → `IdentifiedStructType.gep` → `self.elements[i]`,
对 **opaque**(elements is None)pointee 抛裸 TypeError('NoneType' object is not
subscriptable)。现场:includes/ast/astaux.vp `Keyword.__init__` 的
`self.arg = _copy_str(pool, arg)`,经 HandlesAssign._HandleAttributeStoreLlvm:869
的 GEP 崩溃。成因:同名 struct 存在**两份实例**——函数签名/变量里那份仍 opaque,
类键 `Gen.structs[ClassName]` 上那份已具化;偏移取自类键那份、GEP 却用 self 那份。

- HandlesAssign._HandleAttributeStoreLlvm:GEP 前把 self 指针 bitcast 到**有 body 的
  那份实例**(仅当 self 的 pointee opaque 且类键实例已具化),并把该实例作为
  StructHint 传给 _get_member_offset —— 偏移与 GEP 必须同源(与属性读取路径
  _gep_and_Load_member 的 415916b3 同口径);max_offset 的取值也补上 elements 为
  None 的判据(原实现会对 opaque 实例 len() 触发断言)。
- BaseGen.RequirePointerOperand 新增 RequireConcretePointee(GEP 专用):基址指向
  opaque 具名结构体时 fail-fast,报文给出基址类型、结构体名、GEP 名与当前节点位置,
  不再落到 llvmlite 的裸 TypeError(typed pointer 下"GEP 指向 opaque 结构体"无任何
  合法场景:元素类型必须编译期可知)。

本地冒烟:opaque pointee 被拦下(FailFastError,而非 llvmlite 的 TypeError);
具化 pointee 正常通过。
2026-09-29 22:10:11 +08:00
TermiNexus 4734a54aed fix(tpc): 函数声明创建前自检签名与半注册名字,DuplicatedNameError 不再裸抛
类别 a(编译器缺陷):llvmlite 的 Function.__init__ 顺序是
  NamedValue.__init__ → _set_name → module.scope.register(name)      【名字占用】
  → 逐形参 Argument(...) / ReturnValue(...)(内部 assert isinstance(typ, types.Type))
  → module.add_global(self)                                        【进入 globals】
中间任一步抛错,Python 都不会回滚 scope ⇒ 留下"半注册"名字:module.globals 查无、
module.scope 已占用。而 _EmitFunctionForwardDeclLlvm 的 `except Exception` 在非严格
模式**静默 pass**(严格模式也只 LogWarning)⇒ 真因(返回类型或形参不是 llvmlite
Type,典型 None=注解未解析出来)被吞掉,直到定义阶段 _EmitFunctionLlvm →
_CreateOrReuseFuncDecl 才以 DuplicatedNameError 暴露,报文只有名字、没有两侧信息
(现场:972cf2b7756765d5.ImportsGenMixin._index_imported_module,本次构建 723s 后失败)。

- BaseGen._CreateOrReuseFuncDecl(唯一幂等入口)新增两条**创建前**自检:
  ① 签名自检:return_type 与 args 逐项必须是 ir.Type,否则在名字注册之前就报出
     "哪个位置 / 什么类型 / 完整签名 / 当前节点位置";
  ② 悬空名字自检:name 已进 module.scope 而 module.globals 无对象 ⇒ 直接报出
     "此前同名创建中途失败且异常被吞",不再落到裸 DuplicatedNameError。
- HandlesFunctions._EmitFunctionForwardDeclLlvm:该 except 不再吞非 FailFastError
  异常(原:非严格静默 pass / 严格仅 warning)⇒ 改抛 FailFastError,附函数名、
  本次签名、原始异常类型与消息、当前节点位置。

本地冒烟(5 例全过):合法签名建成功;None 返回类型拦下且不留半注册名字;None 形参
拦下;悬空名字报状态事实;同签名二次调用仍幂等复用。
2026-09-29 20:45:26 +08:00
TermiNexus f28ff1b414 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。
2026-09-29 19:57:57 +08:00
TermiNexus d305d3369b fix(tpc): IR 边界补「指针操作数类型守卫」,非指针值不再抛裸 AttributeError
类别 a(编译器缺陷/诊断缺失):llvmlite 的 IRBuilder.gep 不做任何类型校验,
GEPInstr.__init__ 直接读 ptr.type.is_opaque;LoadInstr 同族亦同(instructions.py
:440/:558)。上游一旦把非指针值(典型 i32)当指针传入,抛出的就是裸
`AttributeError: 'IntType' object has no attribute 'is_opaque'` —— 报文里既没有
指令名、没有操作数、也没有当前语句,无法定位发射点(现场:Phase2 翻译
TransPyV/App/lib/core/Gen/TranslatorGenMixin.vp 时只带 713/787/788 三条语句位置)。

- BaseGen:新增 RequirePointerOperand(IR 边界先行判定,不做兜底转换)与
  _GuardIrBuilder(无条件覆盖 gep/load/store,合法 IR 与原生 ir.IRBuilder 一致);
  _HotspotSiteBuilder 改继承 _GuardIrBuilder;_NewIrBuilder 未启用热点统计时
  由原生 ir.IRBuilder 改为返回 _GuardIrBuilder。
- MemoryOps:_EmitLoad/_EmitStore 的 volatile 分支直接构造 LoadInstr/StoreInstr,
  绕过了 builder.load/store 的类型守卫,补同一判定(非指针 ptr 的 store 会静默
  生成非法 IR 文本)。

报文含:指令名、操作数类型、操作数名、GEP 的 name=(发射点标识)+ 当前节点位置。
2026-09-29 19:24:27 +08:00
TermiNexus 415916b3dd 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 时行为与改前逐字一致;其余偏移消费点(字段写入 / 构造初始化等)本轮未动。
2026-09-29 18:18:49 +08:00
TermiNexus 935a5b372b fix(tpc): 属性读取返回 None 时登记「确切原因」,5 个 return None 点不再同义
类别:a(编译器缺陷 —— fail-fast 报文缺根因区分,5 个不同故障同报"求值为空")

现象:`self.Gen.ExprGen.translate_value(builder, pool, mod, ie.orelse, ...)`
(ExprGenOps.vp:97)报「调用实参编译期求值为空」,而同函数同对象的 `ie.test`
(:43)、`ie.body`(:78)求值正常。三个字段在 `IfExp` 布局里是**连续三槽**
(stub: %"…IfExp" = { i8*, AST*, MemBuddy*, i32×4, list*, AST*(test),
AST*(body), AST*(orelse) },共 11 元素),只有最后那个失败 ⇒ 必然是"偏移越出
body 元素数"这一类,但报文只说"求值为空",无法与"类名未解析 / 基座求值为空 /
结构体未匹配 / stub 无 body"区分。

改动(纯取证,不改变任何判定与 IR):
1. HandlesBase 新增 `_NoteAttrNone(Gen, ClassName, AttrName, Reason)`:把
   `(类名, 字段名, 原因)` 记到 `Gen._LastAttrNone`。
2. HandlesExprAttr 的 5 个 return None 点全部登记:
   - `_gep_and_Load_member`:偏移为 None;**偏移越界**(附 offset / body 元素数 /
     st 版元素数 / 成员表项数)。
   - `_handle_attr_name`:类名未解析;基座自身求值为空;结构体未匹配(附 ObjVal
     类型);stub 无 body;槽位越界(附 offset / pointee 元素数 / st 元素数)。
3. `_DescribeAttrBase` 追加三项事实:`struct body 元素数`、`字段 X 计算偏移`
   (`_get_member_offset`,异常则报异常类型)、以及与被报节点 (类名, 字段名)
   一致时回显 `读取侧原因=...`(不一致不回显,避免嵌套读取的旧记录冒充)。

依据:字段名在成员表里能找到却没读到值,只可能是"偏移 ≥ body 元素数"
(`_get_member_offset` 找不到字段时已 fail-fast,不会返回 None);因此
`偏移/body元素数/成员表项数` 三个数字是判定该情况的唯一依据,必须一次报全。
2026-09-29 17:46:58 +08:00
TermiNexus 0392ccdea0 fix(tpc): fail-fast 报文补「属性基址取证事实」,属性读返回 None 不再靠反推
类别:a(编译器缺陷 —— fail-fast 报文缺取证事实,无法一眼定位根因)

现象:HandlesBase._RaiseUnresolvedExpr 与 HandlesExprCall._RequireArgValue 在
「属性读取值失败」时只报 Node 形态(Attribute),报错点与真因相隔数层调用,
需要人工从 Gen.var_struct_class / Gen.structs / Gen.class_members 反推。

改动:
1. HandlesBase 抽出公开方法 _DescribeAttrBase(Gen, Node) -> str(原 _RaiseUnresolvedExpr
   内联代码抽出,行为不变),对两类基座给事实:
   - Name 基座:var_struct_class / global_struct_class / alloca 类型 / 是否命中
     Gen.structs / class_members 字段数与字段名单(前 12)。
   - Call(func=BinOp(BitOr)) 强转基座:联合基类名 + 短名是否命中 Gen.structs +
     字段数,并显式标注「强转基座本应走 _FailFastAttrCallBase 详细报文,此处仍是本条
     ⇒ 那条 fail-fast 未触发」,把「另一处缺口」这一事实直接写进报文。
2. _RaiseUnresolvedExpr 改调 _DescribeAttrBase(删除内联重复代码)。
3. HandlesExprCall._RequireArgValue 在实参是 Attribute 时追加同一份基址诊断
   (与 HandlesBase 共用,保证两处口径逐字一致)。

不改变任何判定逻辑,仅补取证事实。
2026-09-29 17:40:23 +08:00
TermiNexus b3bc94f99f fix(tpc): 「类 + t.CPtr」联合合并时传播 IsStruct,指针层数不再多算一层
类别:a(编译器缺陷)—— 上游:联合注解 `X | t.CPtr`(X 为类)合并后多出一层指针,
类名随之在下游被当通用指针处理。

根因(一行):
- `_MergeAllComponentTypes` 的"struct 吸收第一个 t.CPtr"规则以 `Result.IsStruct` 为
  **闸门**(该函数 :1022-1027 注释原文:"仅对 struct 类型吸收第一个 CPtr(struct 变量
  默认是指针,AST | t.CPtr = AST*)"):
      base_ptr_count = Result.PtrCount; Result.PtrCount += cptr_count
      if Result.IsStruct and base_ptr_count > 0 and cptr_count > 0: Result.PtrCount -= 1
- 但 `_MergeCTypeInfoInto` 传播 struct 时**只复制 Name、从不置 IsStruct**
  (`if Source.IsStruct and Source.Name and not Target.Name: Target.Name = Source.Name`)
  ⇒ 闸门恒假 ⇒ `list[str] | t.CPtr` 合并后 PtrCount = 1+1 = 2(应为 1)
  ⇒ 得到 `list[str]**` 而非 `list[str]*`。
- 既有同类先例(同一函数紧邻的 REnum 分支)已经写着
  `Target.IsStruct = True` 并注释"同时传播 IsStruct=True,让 CPtr 吸收逻辑生效
  (REnum 在内存布局上是 struct)" ⇒ 通用 struct 分支漏了同一个传播。

后果链(Test/FakeDuckTest/App/main.vp:111/115):
  `parts = s1.split(',')`(`split` 返回注解 `list[str]*` ≡ `list[str] | t.CPtr`)
  声明侧解析成两层指针 ⇒ 值类型 pointee 是指针而非结构体 ⇒ HandlesAssign 的类名推断
  整块(以 pointee 是结构体为前提)跳过 ⇒ `parts` 无 `var_struct_class` ⇒
  `p0: str = parts[0]` 不走 `list[str].__getitem__`,退化成通用指针/字节索引返回整数
  ⇒ 报"整数隐式转指针已禁止"(真因在 4 行之前,报文指不到)。

修复:`_MergeCTypeInfoInto` 里 struct 标志与 Name 成对传播
(`if Source.IsStruct: Target.IsStruct = True`),恢复文档化的"吸收第一个 CPtr"语义。

(上一提交 b6526343 在调用点做了类名恢复,属同族的下游补丁;本次是上游根治,
两者不冲突:上游修好后,参数/字段/返回值等位置不再各自退化。)

验证:GetDiagnostics 干净;未跑编译器。
2026-09-29 17:35:56 +08:00
TermiNexus b652634356 fix(tpc): 裸赋值 RHS 是"联合注解返回"的调用时,从返回注解恢复被指类名
类别:a(编译器缺陷)—— 类型信息被静默丢成通用指针 i8*,后果在数行之后以看不出真因
的报文暴露。

现场(Test/FakeDuckTest/App/main.vp:111/115):
  `parts = s1.split(',')` → `p0: str = parts[0]`
  报"整数隐式转指针已禁止…出错表达式: 下标表达式 Subscript"。

证据链(三段各有代码/探针依据):
1. `_fakeduck.vp:273` 的 `def split(...) -> list[str]*`:只读探针确认 `list[str]*`
   与 `list[str] | t.CPtr` 的 AST **逐字同形**
   (`BinOp(BitOr, Subscript(Name(list)), Attribute(t.CPtr))`)⇒ 注解写法没问题。
2. 该联合形态在 LLVM 层映射成**通用指针 i8***(`list[T] | t.CPtr` 的既有约定,
   见 HandlesAnnAssign:372-383)。
3. `HandlesAssign` 原有的类名推断整块以 `Value.type.pointee` 是结构体为前提
   (:186 的 `isinstance(..., (LiteralStructType, IdentifiedStructType))`)⇒ i8* 直接
   整块跳过 ⇒ `var_struct_class['parts']` 从不登记 ⇒ `parts[0]` 不走
   `list[str].__getitem__`,退化成通用指针/字节索引 ⇒ 结果类型是整数 i8 ⇒ 赋给
   `str` 时触发 int→ptr 拦截。真因位置比报文位置早 4 行。

修复(编译器侧,不要求源码补注解):
- `HandlesExprCall._CallReturnClass(Node, Gen)`:从被调者的 **returns 注解**取回被指
  类名。被调者候选键覆盖三种既有形态(`Name` 函数名、`<接收者类>.<方法>`、
  FakeDuck 影子类 `_str`/`_bytes`、裸 `<方法>`),取第一个能解析出类名的。
- `HandlesExprCall._ClassFromAnnotation(Ann, Gen)`:剔除 `| t.CPtr` 侧取核类型,
  泛型 Subscript 走 `_resolve_generic_slice_type` 构特化名(`list[str]`),并要求该名
  能在 `Gen.structs` / `Gen.class_members` 命中(含 sha1 前缀全名的短名归一);
  取不到一律返回 None —— **不硬塞名字**。
- `HandlesAssign`:仅当"RHS 是调用 + 值类型是通用指针(pointee 非结构体)+
  var_struct_class 尚无该变量"这三个条件同时成立时,才据上恢复登记
  `var_struct_class` / `global_struct_class` / `_var_to_heap_ptr`。其余路径逐字不变。

验证:只读探针覆盖 7 种注解形态(`list[str]*` / `list[str] | t.CPtr` / `Foo*` /
`Foo | t.CPtr` / `ast.AST*` …),提取结果全部符合预期(未命中类名时返回 None);
GetDiagnostics 干净。未跑编译器。

遗留(同族,下一轮):上游"联合注解→i8*"这一步丢类名才是真根因;本次是在调用点
恢复。另外 `parts.__len__()` 在类名缺失时可能被 FakeDuck `_str.__len__` 错接,
修完建议跑一次该测试确认 `split len OK`。
2026-09-29 17:09:42 +08:00
TermiNexus 783aba1e49 fix(tpc): 方法调用/属性调用路径的实参不再静默丢弃(收口"整体错位一格")
类别:a(编译器缺陷)—— 实参求值为空被静默丢弃 ⇒ 后续实参整体前移一格 ⇒ 报出与
真因无关的类型不匹配。

现场(TransPyV/App/lib/core/Gen/Func/FuncGenMixin.vp:1429):
  `return self._lambda_scan_body(trans, ie.orelse, param_names, param_count,
                                 cap_names, cap_allocas, cap_tys, cc6)`
  → `_adjust_args` 报"指针类型 i8* 不能隐式转换为整数参数 i32"。

判据(把报文的"逐项"列表与两种假设逐位对照):
  形参表(stub declare,与该行源码一致):
    self, trans, node=%AST*, param_names=i8*, param_count=i32,
    cap_names=i8*, cap_allocas=i8*, cap_tys=i8*, cap_count=i32
  实测实参:i8*, i32, i8*, i8*, i8*, i32, i32(9 项)
  假设"第 3 个实参(param_names,i8*)被丢弃、末尾由 _fill_default_args 补
  cap_count 的零值 i32"⇒ 预测 arg[2..8]=orelse(i8*), param_count(i32),
  cap_names(i8*), cap_allocas(i8*), cap_tys(i8*), cc6(i32), 补零(i32)
  ⇒ **与实测逐位完全一致**。反证:若丢的是末尾的 cc6,预测序列与实测不符。
  (`_fill_default_args` 只从**末尾**按形参类型补零:HandlesExprCall.py:202-248)

修复:方法调用路径(_HandleMethodCallLlvm)与属性调用回退路径(_HandleCallLlvm)
共 7 处仍写作 `ArgVal = HandleExprLlvm(arg); if ArgVal: append(ArgVal)` —— 正是
`_RequireArgValue` 文档里点名要收口的旧写法(此前只收口了 4 处类构造)。现全部
改为 `_RequireArgValue(...)`:实参求值为空当场 fail-fast,报文含被调者、实参序号与
源表达式,**不丢弃该实参**。

同族未收口(本次不动,逐条记录):`_ApplyKeywordArgs` 的关键字实参(:337)、
`:595` 的 UserCallArgs、`:1262`、`:1336` 共 4 处仍是旧写法。

验证:GetDiagnostics 干净;未跑编译器。
2026-09-29 17:01:14 +08:00
TermiNexus 62cab0f6c1 fix(tpc): 属性取值为空的报文报出"基座 AST 真实形态"(附只读探针反证)
类别:a(编译器缺陷)—— 报文缺事实,且暴露出一个**未触发的 fail-fast**。

现场(TransPyV/App/lib/core/Gen/Expr/ExprGenBase.vp:360,另 231 同族):
  `v = (ast.Expr*(node)).value` — 同函数前三条 `(ast.Assign/AnnAssign/AugAssign*(node)).value`
  同款写法正常,只有 `ast.Expr` 这条报"赋值表达式 'value'(Attribute 形态)编译期求值为空"。

只读探针(vast.parse + BaseHandle._ExprText/_ExprShape,不跑编译器)反证:
  (ast.Assign*(node)).value  -> Attribute(.value←Call(func=BinOp(BitOr, Attribute(.Assign←…), Attribute(.CPtr←…)),args=1))
  (ast.Expr*(node)).value    -> Attribute(.value←Call(func=BinOp(BitOr, Attribute(.Expr←…),   Attribute(.CPtr←…)),args=1))
  **两者 AST 逐字同形**;且 `_ExprText` 对"Call 基座"会退化成裸 attr 名
  ⇒ 报文里的 `表达式 'value'` 并非源码文本,只是渲染退化,不能作为判据。

由此得出一条必须验伪的推论:该形态本应由 HandlesExprAttr._handle_attr_call_result 的
`_FailFastAttrCallBase` 出"失败环节"详细报文(强转基座分支),而 FailFastError 全仓无
`except`(必上抛)——可现场仍是通用报文 ⇒ **那条 fail-fast 未被走到**。

本改动:`_RaiseUnresolvedExpr`(HandlesBase)在 RHS 为 Attribute 时补打
`│ 基址诊断: 基座形态=…`:
- 基座是 Name → var_struct_class 登记名 / alloca 实际 LLVM 类型 / 该名在
  Gen.structs 与 class_members 的命中情况;
- 基座是 `Call(func=BinOp(BitOr))`(强转形态)→ `_extract_union_base_class` 得到的
  联合基类名 + 其 structs/class_members 命中 + 显式标注"该走详细 fail-fast 而没走"。
新增 `BaseHandle._ExprShape`(AST 形态摘要,与 `_ExprText` 的"源码文本"分工互补,
depth≤4)。

验证:只读探针覆盖 7 种基座形态,渲染全部符合预期;GetDiagnostics 干净。未跑编译器。
2026-09-29 16:55:41 +08:00
TermiNexus 716cd61a75 fix(tpc): 属性取值失败的报文补"基址诊断" + 补掉声明加载去重签名的真实缺口
类别:a(编译器缺陷)—— 两者都是"缺事实/漏输入",不是放宽判定。

【一】`_RaiseUnresolvedExpr` 增打基址事实(HandlesBase)
现场:TransPyV/App/lib/core/Gen/MatchGenMixin.vp:231
  `val: ast.AST* = expr_node.value`(base 是 `expr_node: rawptr[ast.Expr]`)
  → Attribute 取值返回 None → 本报文 + sys.exit(1)。
属性读回 None 的根因只有两类:① 类名登记退化(var_struct_class 存 'rawptr' /
'rawptr[X]' / 折叠态,或压根没登记);② 登记名与 Gen.structs / class_members 的键
不同源(短名 vs sha1 全名)。原报文只有"目标变量 + 注解 + 源码",不含任何一项事实
⇒ 只能从"求值为空"反推。现追加一行(仅 Attribute + Name 基址时,其余形态逐字不变):
  基址诊断: 'expr_node' 的 var_struct_class=<...>;alloca 类型=<...>;
            该名在 Gen.structs=<是/否>;class_members=<N 个字段/无该键>
与 `_DescribeMemberSlot` 同口径:列数据、不猜。

【二】`_ClassMembersReloadSignature` 的"类数"改为"逐类 (键, 表长) 元组"(HandlesImports)
上一轮 3399b44c 的签名里全局进度只记 `len(class_members)`(键数)。缺口:**某已有类的
成员表继续增长时键数不变**(典型=父类成员后到),而父类成员正是继承前缀的唯一来源
⇒ 极端时序下会漏掉一次必要重跑。改为逐类 (键, 长度) 元组,并补记
`len(class_parents)` / `len(class_parent)`(父链登记本身也是输入)。
代价 O(类数)(约 300 项/次调用),仍远低于一次重跑的成本。

验证:GetDiagnostics 干净;py 层 import + 既有 16 例类型判定 smoke test 全过。
未跑编译器(无 includes 改动,无需重编)。
2026-09-29 16:50:51 +08:00
TermiNexus aa1b536517 perf(tpc): _generate_structs 去掉两处"纯 no-op 重建",不改任何判定语义
类别:b(机制漏缓存)—— 都是"结果等价却重算"的浪费,不是判定逻辑改动。

证据(PROF\main_prof_72472.prof 的 `_generate_structs` 子调用归属,self=39.07s/8,493 次):
  isinstance 6.195s(6,249 万次)· `HandlesBase.__get__` 3.596s · len 2.892s
  `_TypedPointerType.__init__` 2.600s(375 万次)· `_patched_new` 2.030s · dict.get 2.555s
  `_MemberSlotAccepts` 2.437s · SymbolTable.lookup 2.105s · rsplit 1.123s

改动(均为**恒等变换**,任意中断/交错下语义不变,不引入失效判定):
1. Step 3 成员类型重绑:新增 `if pointee is not struct:` / `if member_type is not struct:`
   短路。原实现对"pointee 本来就是该 struct 实例"的成员也**现建一个 ir.PointerType
   再覆盖写**——llvmlite 类型是值语义(`_TypedPointerType.__eq__` 按 pointee 比较、
   打印只取决于结构),覆盖写是纯 no-op。该分支每次调用遍历全部类的全部成员
   ⇒ 这是 375 万次指针构造(约占本函数 9s,含 _patched_new/_patched_init/_IsBadPointee)
   的主源。
2. T* 占位扫描改为 `_StructBodyHasPlaceholderT`(按 body 对象记忆):Step 4 的
   "全类×全元素"扫描与 set_body 前的复核扫描,每次调用都全量重扫,是本函数
   6,249 万次 isinstance 与 498 万次 rsplit 的主源。判据精确性:该扫描是
   `st.elements` 的纯函数,而 `elements` 在 llvmlite 里**整体替换**(set_body 赋新
   tuple、作废置 None,从不原地改元素)⇒ 用 `elements` 的对象身份当版本号即可精确
   判定"body 未变";缓存条目强引用 struct 与 elements,二者存活 ⇒ id 不会被回收
   复用 ⇒ 不会误判命中。
3. vtable 槽 `i8*` 改用模块级共享实例 `_I8_PTR`(原来每类每次现建)。

**刻意不做**:不加"全量状态签名 early-out"。理由(写在此处备查):签名只能覆盖
"每类成员个数"这类可 O(类数) 取得的量,覆盖不到"成员类型原地替换";而那次替换正是
本函数 Step 3 自己做的 ⇒ 一旦某轮在中途抛异常,签名与上一次成功轮相同,early-out
会跳过**必要的**重建。要做到完备就得给约 25 处 `class_members` 变更点埋版本号,
收益不确定、风险实在,故不做。

验证:py 层 import + GetDiagnostics 干净;未跑编译器。
2026-09-29 16:37:03 +08:00
TermiNexus 3399b44c2d perf(tpc): 声明加载去重 + _parse_llvm_type memo(PROF main_prof_72472 前两名热点)
两项改动同在 HandlesImports.py,合并提交:都属"重复重跑"型浪费,且第二项是第一项
放大链的下游。

【一】声明加载按依赖签名去重(类别 a:编译器缺陷)
证据:_TryLoadClassMembersFromPyi 142,888 次 → 6,683 次落到 _EmitExternalClassDeclLlvm
(cum 103.1s)→ _generate_structs 8,059 次(cum 128.7s)→ _TryLoadStructFromStub
88,970 次 → _parse_llvm_type 909,382 次;对照模块声明发射仅 320 次 ⇒ 绝大多数是
同状态的重复发射。
根因:guard 只在"表非空 **且已登记已展平**"时短路,而"父类是泛型特化/始终不可得"
的类永远不会被 `Gen._members_flattened` 登记 ⇒ guard 永不命中 ⇒ 每次重入都重跑整套
`_EmitExternalClassDeclLlvm`(其内部又触发 _generate_structs → _TryLoadStructFromStub
→ 本方法,即每轮 4 帧的环)。
修复:新增 `_ClassMembersReloadSignature`(纯读)收录"重跑能否产生新结果"的全部输入
——全局进度(已登记类数/已展平类数)、本类成员表长度与展平登记、struct body 元素数、
各父类的成员表长度与展平登记;签名不变即直接短路,依赖一变立刻放行(不破坏"与父类/
struct 就绪顺序无关"的设计目标;与 EnsureClassMembersFlattened 的依赖签名同一思路)。
要点:键含 source_sha1(同名类分属不同模块);**先登记再发射**(否则环上第一层重入
还会再发一次,去重被推迟一层);发射抛异常时撤销登记(保住"失败可重试",不让失败态
被签名冻结);force=True 完全绕过。

【二】`_parse_llvm_type` memo + 基础类型表提升为模块常量(类别 b:漏缓存)
证据:该函数被调 1,689,809 次(cum 46.7s),`type_map` 字典每次调用现建 ⇒ 字典字面量
1.69M 次、`IntType.__new__` 9.09M 次;每个 `X*` 还要新建 `ir.PointerType` ⇒ 全程序
`_TypedPointerType.__init__` 4,596,463 次(≈12–15s)主要来自这里。
修复:拆 memo 包装 + `_parse_llvm_type_impl`(原实现逐字搬移,内部递归仍走包装);
memo 只缓存**与 Gen 状态无关**的结果(新增 `_IsGenIndependentLlvmType`:类型图不含
`IdentifiedStructType` 且非 None——含具名结构体的结果绑定 `Gen.structs` 实例且有
泛型特化副作用,None 表示"此刻未加载",二者都不可缓存,理由写进注释防以后被放宽);
`_LLVM_PRIMITIVE_TYPE_MAP` 提升为模块级常量(llvmlite 中这些类型无状态、IntType
按宽度缓存同一实例,跨调用/跨 Gen 共享安全)。

验证:py 层 smoke test 覆盖 16 种类型(含 struct / struct* / [4 x struct] /
{i32,struct} / fn(struct) / fn->struct* 组合),判定全部符合预期,模块可正常 import。
未跑编译器(无 includes 改动,无需重编)。
2026-09-29 16:31:29 +08:00
TermiNexus 030d0fd149 feat(tpc): PROF 输出按编译启动时间分目录,不再清理旧目录
需求(2026-09-29):PROF 按启动时间分组新建文件夹生成,不清理旧的 PROF。

改动:
- Phase2Translator:新增 `StartProfRun()`(在 PROF 根目录下按 `YYYYmmdd_HHMMSS`
  新建本次编译专属子目录,同名补 `_N`;失败回落 PROF 根目录)与
  `GetProfRunDir()`(主进程用已选定目录,worker 用继承的环境变量
  `TPC_PROF_RUN_DIR` 复用同一目录)。原 `_CleanProfFiles()` 删除——它的语义
  是"每次编译删掉 PROF 下全部 *.prof",与"保留历次"直接冲突。
- worker_prof_*.prof 落 `PROF/<启动时间>/`。
- main_prof_{pid}.prof 与 main_time.txt 同样落该目录(Projectrans.py)。
- Config.main 启动时调用 StartProfRun 并打印本次输出目录。

子目录名经环境变量传子进程:ProcessPoolExecutor 用 spawn 起新解释器,进程内
模块态不共享,环境变量随进程继承 ⇒ 同一次编译的所有 worker 落在同一目录,
不会各建各的。
2026-09-29 16:26:44 +08:00
TermiNexus 467e62a45e fix(tpc): 继承前缀配槽改用 elements 快照,不再对已被重置为 None 的 body 下标
类别:a(编译器缺陷)—— 裸 Python TypeError 崩溃 + 外层静默吞掉整条类声明。

现场:编译 includes/ast/astaux.vp 时
  HandlesImports.py:2526 `_inherited_ph.append((_nm_ph, _st.elements[_slot0_ph + _i_ph]))`
  → TypeError: 'NoneType' object is not subscriptable
(栈由 VLogger.warning 打印 exc_info 后首次可见;外层 HandlesImports.py:647
 `except Exception` 只 WARN "忽略节点 ClassDef" 就 continue ⇒ 该类声明整条
 被静默丢弃,类型解析退化。)

根因(TOCTOU):guard 在 :2500 通过(elements is not None),但两侧之间
`_ResolveInheritedFieldNames`(:2535)会加载父类成员:
  _ResolveInheritedFieldNames → _TryLoadClassMembersFromPyi
  → _EmitExternalClassDeclLlvm → _generate_structs
而 `_generate_structs` 判定"struct body 与 class_members 不一致"时会**主动**
`existing_st.elements = None`(StructGen.py:579/:590,既有机制,为强制重新派生)
⇒ 配对时读到的 elements 已变 None。

修复:guard 通过时立即对 elements 取快照 `_elements_ph`,槽位数与槽位类型取自
**同一时刻**的布局(`_num_elements` 本就取自它,二者必须同源);配对用快照,
不再触碰可能被重入重置的 `_st.elements`。无重入时行为与改前逐字节一致。
2026-09-29 16:26:43 +08:00
TermiNexus 24573e5af6 fix(tpc): HandlesImports 不再静默 RecursionError + VLogger.warning 补打 exc_info
类别:a(编译器缺陷)+ b(机制漏豁免)—— 类型解析链成环被当成"可忽略异常"吞掉,
上游根因不可见、声明静默退化。

- HandlesImports.py:978 / :1475 / :1583 三处新增 `except RecursionError: raise`,
  对齐家族口径(HandlesBase.py:1060-1068:「RecursionError 不是可忽略异常,
  是类型解析/声明发射链成环的真缺陷信号」)。成环路径:
  _TryLoadClassMembersFromPyi ↔ _EmitExternalClassDeclLlvm ↔ _TryLoadStructFromStub
  (每轮 4 帧),现场与 :2985 深探针记录一致(match.MatchOr.patterns、
  stmts.Interactive.body 等"首字段即指针/容器"的类)。
  修复前表现为一行 `WARN: HandlesImports: 忽略异常 maximum recursion depth exceeded`
  + 返回类型退化成 CInt / 形参退化成 i32。

- VLogger.py:warning() 的 exc_info 形参**从未被使用** —— 调用方传了异常却无任何
  输出,吞异常点只剩一行 WARN、栈不可见。现与 error() 同口径打印
  `Exception: <类型>: <消息>` + traceback(重复帧由 traceback 模块自动折叠为
  "[Previous line repeated N more times]",不刷屏)。
2026-09-29 16:16:01 +08:00
TermiNexus d3537c7b3f fix(tpc): 局部 AnnAssign 反向重建不再窄化「基类指针」声明为派生类
【类别 a 编译器缺陷】局部带注解赋值的「反向重建」缺少方向判定,
把源码显式声明的**基类指针**落地成**派生类指针**,
再用新收紧的指针隐式转换判据翻译后续赋值时就必然失败。

现象:
  翻译 includes/ast/parser.vp 报
  「指针隐式转换已禁止(非基本类型(结构体/类/联合)):不能把指针类型
   %"….AST"* 隐式转为 %"….Name"*」,
  出错语句 `_parse_atom` 内 `node = _new_const_str(...)`(parser.vp:1921 附近)。

根因链(全部可取证):
  1. `node` 在 _parse_atom 内首次声明于
     `node: rawptr[AST] = Name(ps.pool, name_str, ASTCtx.Load)`(parser.vp:1899),
     注解写明的被指类是 **AST(基类)**,RHS 是 **Name(派生类)指针**;
  2. HandlesAnnAssign._HandleAnnAssignLlvmInner 中「以 RHS 被指类型反向重建
     alloca」的代码块(初版快照即存在,注释自述本意是「注解退化成裸基类 / i8*,
     RHS 构造出精确特化 ⇒ 用 RHS 反向升级」)**无条件**执行:
        if isinstance(VarType, ir.PointerType) and VarType.pointee is not ST ...:
            var = Gen._allocaEntry(ir.PointerType(pointee), name=VarName)
      ⇒ node 的栈槽被落成 `Name*` 而非注解的 `AST*`;
      (既有仅有的方向判据 `_rb_is_bare_generic`,commit 0ffaadc9,只拦「RHS 是
      裸泛型基类」这一个方向,拦不住「RHS 是注解被指类的派生类」)
  3. 旧构建产物实测确认槽类型确为派生类指针:
     Test/AstTest/.tpv_build/temp/05/056a6a1271f871d2.ll 的
     `@"…._parse_atom"` 内 `%24 = alloca %"8f3ee13abe0c5527.Name"*`,
     且 `_parse_fstring` / `_new_const_str`(均返回 AST*)的返回值后面紧跟
     `bitcast …AST* → …Name*` 再 store —— 即 AST*→Name* 的**下转**此前是静默
     bitcast;2026-09-28 指针隐式转换收紧(结构体只允许上转)后,同一处变成
     fail-fast,暴露出上层这个窄化缺陷。收紧本身正确,缺陷在窄化。

修法(与既有 `_rb_is_bare_generic` 对称的「方向判定(其二)」,非兜底):
  · 新增注解侧被指类快照 `_ann_pointee`:优先取已解析出的 `VarType.pointee`;
    注解被 TypeMerge 折叠成 i8*(rawptr[X] / t.CPtr[X])时取同一函数上方
    635-684 已恢复进 `var_struct_class` 的 X(那正是注解写明的被指类);
  · 仅当 `_IsInheritanceUpcastPointee(RHS 被指类, 注解被指类)` 为真
    (即注解就是合法的「基类指针」声明、RHS 只是继承上转)时**跳过重建**,
    保留注解类型,杜绝「按 RHS 窄化声明类型」。
  判据只拦这一个确定的方向错误:注解退化成 i8*/裸占位(被指类非 RHS 的基类)
  时 `_rb_is_upcast` 为假,泛型特化升级路径(commit 0ffaadc9 修的现场)不受影响。

影响面:本处是 TPC 内唯一的「以 RHS 重建 alloca」点(全仓 grep 该调用仅此一处)。
窄化取消后,槽类型回归注解声明;后续赋值/取值按正常隐式转换判定
(派生→基类为上转,放行),不再出现「先被悄悄窄化、再被判下转」的自相矛盾。

1 file changed, 31 insertions(+), 1 deletion(-)
2026-09-29 14:53:33 +08:00
TermiNexus ac0d0f2cc8 perf(tpc): _MemberSlotAccepts 身份短路 + _generate_structs 热路径局部化(profile 第二梯队)
依据 PROF/main_prof_10152.prof(tottime 2288.9s / 72 文件顺序翻译):

  _generate_structs        自身 268.6s   cum 1893.2s   15762 基本/35240 总调用
    └ 子调用:_TryLoadStructFromStub 1589.5s×418k、_MemberSlotAccepts 61.1s×54.2M
              isinstance 39.7s×300M、PointerType 构造 68.7s×15.9M、
              SymbolTable.lookup 16.7s×15.9M、__len__ 6.8s×15.9M

本次两处(均等价改写):
1. StructGen._MemberSlotAccepts:在 `Declared == Slot or str(Declared) == str(Slot)`
   之前加**身份短路** `Declared is Slot`。该函数被 _generate_structs 调 54.2M 次,
   其中 llvmlite `Type.__eq__` 合计 ~30s(types.py:178/303/550/720)+
   `str()` ~3.3s;同一对象(body 与 class_members 引用同一类型对象)是其常见形态。
2. StructGen._generate_structs:把 `self.SymbolTable` / `.lookup` 提升为函数级局部
   (真值语义逐字保持:SymbolTable 定义了 __len__,故先取 `bool(_symtab)`),
   消掉逐类重复的属性查找与 15.9M 次 `__len__`。

说明:本提交只做等价微优化;本函数的大头(35,240 次全量 O(类数×成员数) 扫描、
429k 次结构体 stub 加载、1407 次泛型特化整类发射)需要结构性改动,另行评估。
2026-09-29 14:41:48 +08:00
TermiNexus 4fc067e3bf perf(tpc): 按 profile 消掉两处主热点(stub 索引每 miss 重扫目录 / vtable 每类全扫函数)
依据 PROF/main_prof_10152.prof(main_time.txt: 失败 2362.46s,72 个文件全部走
顺序 _translate_file,tottime 合计 2288.9s):

  tottime      calls   函数
  491.7s      723782   nt._path_isdir            ← 总时长 21%
  268.6s       15762   StructGen._generate_structs
  182.9s      479819   nt.listdir                ← 8%
  87.2s    657640754   builtins.isinstance
  78.6s     41514263   ntpath.join
  75.9s     83047878   ntpath.splitroot
  75.7s         1631   StructGen._create_Vtable_globals   (46ms/次)
  66.6s       238007   HandlesImports._build_stub_index
  63.0s    317868145   str.startswith
  38.4s    175751378   str.endswith
  ...
  ── 仅 isdir + listdir + join/splitroot/endswith ≈ 850s ≈ 总时长的 37%

一、_build_stub_index 的零 syscall 快路径(HandlesImports + Phase2Translator)
    它由 _TryLoadStructFromStub 的**每次索引 miss** 调用(238k 次),每次都要
    2× os.path.isdir + 2× os.listdir + 全量 endswith/join 重拼文件集合 ——
    这正是上面 isdir 723k / listdir 479k 的主要来源。
    改为:写入方显式置脏(ImportHandle.MarkStubSetDirty),未置脏时直接返回。
    置脏点:① 每个文件翻译入口(_translate_file,顺序与 worker 模式皆按文件粒度
    覆盖)② 写 temp/<sha1>.stub.ll 之后 ③ 注册 include 写 output/<sha1>.stub.ll
    之后。索引建完即清脏。语义:增量重扫、output 覆盖 temp 全部不变,只是把
    "集合是否变化"由目录扫描改为写入方免费告知(编译进程内 stub 只增不改)。

二、_create_Vtable_globals 的 O(类数 × 函数数) → O(函数数)
    原实现对每个候选类都遍历一遍 self.functions 找 "{ClassName}." 前缀且非
    declare 的函数;改为一次性预计算"非 declare 函数名的全部点分前缀"集合,
    每类只做一次集合查询。判据等价(startswith(prefix) ⟺ prefix 在点分前缀集)。

重编后请再抓一次 profile 复核收益;下一梯队热点是 _generate_structs
(268.6s tottime / 35k 次调用)与 _parse_llvm_type(3.5M 次调用)。
2026-09-29 14:36:57 +08:00
TermiNexus f1e7fd6c28 perf(tpc): _LookupStubFuncType 的目录计数按目录 mtime 缓存,去掉每次调用的全量 listdir
类别:纯提速(判定结果不变)。原实现每次调用都要

  sum(1 for f in os.listdir(output_dir) if f.endswith('.stub.ll'))
  sum(1 for f in os.listdir(temp_dir)   if f.endswith('.stub.ll'))

只为构造一个"stub 是否新增"的签名;本函数位于每次跨模块函数/方法查找的热路径
(20 个调用点),开销 = O(目录文件数) × 调用次数,与编译工作量无关,工程越大越亏。

改为 ImportHandle._CountStubFilesInDir(DirPath):同一「目录 + 目录 mtime_ns」
只 listdir 一次,mtime 命中即复用上次计数(每目录一次 os.stat 代替一次
os.listdir + 全量 endswith 判定)。
判据:stub 目录只增不改、且由主进程在批次之间写入 ⇒ 只有目录条目增删才会改变
计数,而 Windows/NTFS 在条目增/删/改名时会更新目录 mtime。
取不到 stat(目录不存在/权限)时不写缓存、退化为每次实算。
计数仍是权威签名,缓存只决定"要不要重新 listdir",不改变任何判定结果
(增量重扫、output 覆盖 temp 的语义逐字不变)。
2026-09-29 14:32:39 +08:00
TermiNexus f4c8c48748 fix(tpc): 函数声明创建全部收口到唯一幂等入口,消除 DuplicatedNameError 裸抛
类别 a(编译器缺陷)+ b(机制漏豁免)。报错:翻译 lib\core\Gen\Expr\ExprGenName.vp
失败,报文只有名字 dddc595175f00c66.ExprGenAttr.find_global_in_module
⇒ llvmlite DuplicatedNameError(NameScope.register 对 GlobalValue
deduplicate_name=False,同名 global 再建必抛,__str__ 只回名字)。

根因(已取证,非猜测):
- llvmlite Module 里 `scope`(NameScope,注册/去重)与 `globals`(OrderedDict)
  是两套结构;`add_global` 只在构造时写入 ===> 「名字已在 scope._useset、
  但 globals 查不到」这一状态一旦出现,任何"先查 globals 再建"的守卫都会失效。
- 该状态由**清理不完整**制造:删旧名时只删"对象真名 existing_func.name/old_name",
  而字典键/目标名(func_name / MangledName)仍留在 scope._useset。
  别名注册(Gen.functions[短名] = 名为全名的对象)恰好让二者不同名。
- 另有若干创建点只看 Gen.functions、完全不看 module.globals(裸建 ir.Function),
  只要 module.globals 已有同名声明就裸抛 DuplicatedNameError。

本次收口(函数声明创建的唯一入口 = BaseGen._CreateOrReuseFuncDecl):
1. BaseGen._CreateOrReuseFuncDecl:新增"名字被非函数 global 占住"分支,把冲突
   双方(类型 + repr)报出来,替代原 llvmlite 只给名字的裸抛。
2. 清理不完整处补全为「同时清 真名 + 目标名」:
   - HandlesImports.py 模板 declare 装载(clearing func_name 之外还清 existing_func.name)
   - HandlesFunctions._replace_function_decl(clearing {old_name, MangledName})
   - 其 except 对 FailFastError 改为上抛(不再回退成复用旧声明)
3. 裸建 ir.Function 的创建点改走唯一入口(module.globals 同名则复用、
   签名冲突/非函数冲突则 fail-fast):
   - HandlesExprCall._HandleStaticMethodCallLlvm({sha1}.Class.Method)  ← 本现场形态
   - HandlesExprCall._HandleMethodCallLlvm 的 class_sha1 回退分支
   - HandlesExprCall._try_declare_c_lib_func / HandlesExprBuiltin._try_declare_c_lib_func
   - HandlesAssign 模块级全局赋值处的函数声明
   - HandlesImports.__before_init__ 声明(原查 NewFuncName 却建 MangledName)
   - HandlesFunctions._EmitFunctionLlvm(补登记 module.globals 已有声明,
     使既有的签名修复路径生效,而不是裸建撞名)

行为等价性:上述各点原先"撞名"时必然抛 DuplicatedNameError(即必失败),
收口后要么复用同一符号、要么 fail-fast 报出两侧签名/冲突类型,无静默降级。
2026-09-29 13:50:44 +08:00
TermiNexus 8c831a38d3 fix(tpc): 局部 str*/str|t.CPtr 注解不再被压平成 i8*(补 PtrCount<=1 豁免)
类别 b(机制漏豁免):`TypeInfo.IsStr` 对**任何** CChar + 指针层都为真,
局部 AnnAssign 处却无条件把 PtrCount 压成 1,导致 `argv_arr: str*` 被声明为
i8*,而同名模块级全局 `_argv: str*` 走 HandlesAssign._EmitGlobalAnnAssignLlvm
的 FromNode 得到 i8**,于是 `_argv = argv_arr` 被
「指针隐式转换已禁止(层数不同):i8* → i8**」拦下(includes/sys.vp:114、
_init_argv_posix 同款 L148)。

同一规则在 HandlesFunctions.py:599 / 690 / 722 / 1434 / 1514 / 1602 / 1662 与
HandlesImports.py:1440 / 1542 都已带 `IsStr and PtrCount <= 1` 豁免(注释写明
"str | t.CPtr 合并后 IsStr=True 但 PtrCount=2(i8**,如 argv 数组),不能压平"),
唯独 HandlesAnnAssign.py:452 漏了这一条 —— 本次补齐:只压平 PtrCount<=1,
`str*` / `str | t.CPtr` 保持 i8**;_str 影子结构与 var_ptr_element(8 字节步长)
标记仍照旧执行。

判据(不猜):
1. TPV 参考实现产物 Test/SysLibTest/.tpv_build/temp/0c/0cc16828640f94b7.ll:
   `@"0cc16828640f94b7._argv" = global i8**`、_init_argv_win32 内
   `alloca i8**`(argv_arr)、`store i8** %x, i8*** @"..._argv"` —— 局部与全局
   同为 i8**。
2. TPC 全局侧 FromNode 对 `str*` 给出 PtrCount=2(CChar + str 的 1 层 + CPtr 的 1 层)。

重编即可验证 sys.vp 第 84/114 行一致。
2026-09-29 13:33:54 +08:00
TermiNexus 25cda80995 fix(tpc): 补齐单/多参泛型类下标注解(list[str] → %"{模板sha1}.list[str]"*)
类别:a 编译器缺陷(机制缺失:两个注解解析器都只认双参定长 list[T, N])。

现场(前序报错):CallGenMixin.vp:149 调 ClassGenGeneric._specialize_generic_class
报「%\"650c1c3366d09e39.list[str]\"* 不能隐式转 i32」——实参侧正确、**形参侧
`type_args: list[str]*` 在符号表里成 i32**。同类:字段
`TypeGenBase.CTypedefNames: list[str] | t.CPtr` 在共享符号表里成 i8*(实测
temp/_shared_sym.json)。

真根因:单参泛型类下标在两处注解解析器里都无人处理,直接落到"未知类型"兜底:
  · TypeAnnotationResolver._from_node_subscript(IsListAnnotation 只认双参)
    → `return CTypeInfo()`(空 ⇒ i32 / 与 t.CPtr 合并成 i8*);
  · LLVMTypeMapper._resolve_subscript → warn + 'i32'。
而声明端 DeclarationGenerator._get_type_str 的泛型分支(L1393-1423)**一直是对的**
(`%"<模板sha1>.<特化名>"*`),三处规则分裂。

修复(一条规则源 + 两处消费 + 一处物化):
1. SymbolUtils 新增 ResolveGenericClassSpecName / IsGenericClassSubscriptBase ——
   与 DeclarationGenerator 泛型分支逐字同口径:
   spec = 基名 + '[' + ']['.join(ResolveGenericTypeArgStr(实参)) + ']';
   判据=该基名**有类定义**(符号表条目 IsStruct 或任一命名空间命中,实测 list
   条目 IsStruct=True,故等价覆盖声明端 non_struct_types ∪ _generic_class_names)。
   另加 GetStructSha1MapFromSymtab / GetTCAliasMapFromSymtab(从
   SymbolTable.translator 取 Phase2 已挂好的 _struct_sha1_map /
   _t_c_imported_names),供实参串跨模块类引用解析('<sha1>.AST')。
2. TypeAnnotationResolver._from_node_subscript 末尾(既有 rawptr/CPtr/CArray/
   list[T,N]/tuple 分支**之后**)接入:产出
   CTypeInfo(BaseType=CStruct(spec), IsStruct=True, Name=spec)(PtrCount 保持 0,
   与本文件既有结构体 promotion 同口径:`X | t.CPtr` 取 max→1 层、
   `rawptr[X]` +1→1 层,均与 declare 侧一致)。
3. LLVMTypeMapper._resolve_subscript 同样位置接入:产出 `%struct.<spec>*`
   (与 _resolve_name / _resolve_attribute 的类引用同一表示)。
4. BaseGen._get_or_create_struct:特化名(含 '[')先按**基名**查
   _struct_sha1_map 取模板 sha1 再拼 IR 名 —— 否则会用**使用方模块** sha1 拼出
   '<本模块sha1>.list[str]',与 declare/define 端 '<模板sha1>.list[str]' 分裂。

自测(vast AST):list[str]→'list[str]'、dict[str,int]→'dict[str][int]'、
list[rawptr[AST]]→'list[AST*]'、rawptr[int]/tuple[…]→None(仍由既有分支处理)。
2026-09-29 13:17:36 +08:00
TermiNexus a6c9003a19 fix(tpc): struct body 解冻改用 llvmlite 公开属性 elements(原 _elements 是空操作)
类别:a 编译器缺陷(机制漏豁免:错误的"解冻"字段名使所有 body 重建路径失效)。

现场(连续多轮复现):TransPyV Generator.vp:200
`self.ExprGenFstr = SubExprGenFstr` 报「指针隐式转换已禁止(层数不同):
%"a7b68c79f39cb4d3.ExprGenFstr"* → i8**」,[成员槽来源] 显示
class_members[Generator] 下标 11 声明槽类型**正确**,而布局
'47382dc8277c9d28.Generator' 的 elements[11]=i8**。

取证(已核对全部产物与共享符号表,排除"来源侧退化"):
- TransPyV/output/*.stub.ll 与 temp/47382dc8277c9d28.stub.ll 里
  %"47382dc8277c9d28.Generator" 的 slot11 全部是 %"a7b68c79f39cb4d3.ExprGenFstr"*,
  **没有任何 stub 含 i8**;
- 共享符号表(temp/_shared_sym.json → symbol_table_bytes)里
  Generator.Members['ExprGenFstr'] = CTypeInfo(BaseType=CStruct('ExprGenFstr'),
  PtrCount=1, IsStruct=True),也**正确**。

真根因:llvmlite 本版本 IdentifiedStructType 的字段是**公开属性 `elements`**
(types.py: `self.elements = None`;`is_opaque` = `elements is None`;
`set_body` 仅在 `elements is not None` 时抛 "already defined")。而本仓所有
"清除 body 以便重建"的路径都写 `st._elements = None`(不存在的属性)⇒ **空操作** ⇒
⇒ `if struct_type.elements is None` 恒假 ⇒ set_body 永不执行 ⇒ 早期冻结的错误布局
(如 i8**)无法被任何后续权威重建覆盖:
  · StructGen Step 4 逐元素校验已判定不一致(_slot_types_ok=False),清空失败 ⇒
    既不能 skip 也不能重建 ⇒ 错布局被静默保留;
  · HandlesClassDef 的本地定义权威重建点(class_members 已重建为正确类型后)
    同样无法刷新 body。
两处叠加正是"class_members 正确、布局却是 i8**"的稳定现象,也解释了此前
TypeGenCDef(槽3) / ExprGenFstr(槽11) 在不同构建里交替出现(取决于谁先被冻结)。

修复:四处解冻改为写公开属性 `elements`:
  · StructGen.py Step 4 前置校验失败后的清空;
  · StructGen.py sha1 全名 key 的 struct 清空;
  · StructGen.py T* 占位清空;
  · HandlesClassDef.py @t.NoVTable 本地定义点的清空。
清空后 is_opaque 恢复为真 ⇒ _TryLoadStructFromStub(declare 侧权威 body)或
class_members 重建可真正 set_body。
2026-09-29 13:13:17 +08:00
TermiNexus 9d8aedec4a fix(tpc): 函数声明创建收口到幂等入口 _CreateOrReuseFuncDecl;清理 DDEBUG 探针
类别:a 编译器缺陷(根因修复)+ 调试探针移除。

现场:翻译 CallGenFd.vp 时抛 llvmlite.ir._utils.DuplicatedNameError:
'5d84ea579d83ef73.CallGenFd._translate_fakeduck_call'(llvmlite 的
NameScope 按名字去重,GlobalValue.deduplicate_name=False ⇒ 同名 global
建第二次必抛,且报文只给名字、不给两侧签名)。

根因:BaseGen._get_or_declare_function 建完 ir.Function 后**只加入
module.globals**(llvmlite 自动)而**未登记 Gen.functions**;而
HandlesFunctions._EmitFunctionLlvm 的 need_new 判据只看 Gen.functions
(`MangledName not in Gen.functions`)⇒ 同一名字再建一次 → DuplicatedNameError;
该处 except 又把它吞掉(非 strict 模式只 LogWarning)→ 函数始终没进
Gen.functions → 后续其它创建点(FuncGen._get_or_declare_func /
get_or_declare_c_func,同样只查 self.functions)再建 → 异常从别处逃逸到
Phase2Translator。

修复:
1. BaseGen 新增 _CreateOrReuseFuncDecl(name, func_type) —— 函数声明创建的
   唯一幂等入口:module.globals 已有同名函数且**函数类型结构串相同**则复用
   (并补登记 Gen.functions;FunctionType 无 __eq__,只能比 str);
   类型不同则 fail-fast 报出两侧签名(declare/define 分裂的真因),
   绝不静默复用错签名(调用点会按错签名传参)。
2. _get_or_declare_function 改走该入口(顺带补登记 Gen.functions);
   FuncGen._get_or_declare_func / get_or_declare_c_func 改走该入口;
   HandlesFunctions 定义路径改走该入口,且该处 except 对 FailFastError
   直接 re-raise(不再吞)。
3. 清理遗留调试探针(含 sys.exit(1)):BaseGen 三处 [DDEBUG]
   (_get_or_declare_function 双点 / _mangle_func_name 前导点与结果双点)、
   HandlesExprAsm 两处 [DDEBUG_ASM]、HandlesExprCall 一处
   [DDEBUG] _HandleClosureCallLlvm。
2026-09-29 13:00:11 +08:00