fix(transpyv): 修正 mark_module_class 的表生命周期(ASan 悬空键崩溃)

类别:a(编译器缺陷 —— 上一提交自引入的跨文件悬空 HashTable)

现场:ASan access-violation(READ @0x1),rcx=0x1
  string.strcmp (includes/string.vp:86)
  → HashTable._find_slot (_resize) → __setitem__ → set_int
  → StructGenMixin.mark_module_class → ClassGenDef.translate_class_def
  → TranslatorGenMixin.translate_children → Translator.translate → RunPhase1 → main

根因:`ModuleClasses` 原先在 `mark_module_class` 里**懒创建**,并长期挂在 Gen 上;
表体与键都取自当次文件的 pool,而 pool 在文件/遍切换时会被 reset ⇒ 下一个文件里
`_resize` 重新散列旧键时 strcmp 读到已回收内存(0x1)⇒ 崩溃。

改动(与既有 `VisibleStructs` 同口径:表与键同池、同文件生命周期):
1. `ModuleClasses` 改由 `reset_visible_structs`(每文件翻译前调用)**无条件新建**
   (不再区分 strict 分支),与键同池 ⇒ 无悬空。
2. `CurDefModuleSha1` 指向 `trans.ModuleSha1`(同一 pool 的串),同样在该函数里
   清空,避免下一次 strcat/strcmp 读已回收内存。
3. `mark_module_class` 不再懒建表:池或表缺失时只记 `CurDefModuleSha1`、跳过登记
   (宁可退化为改前行为,也不冒悬空风险)。

语义不变:规则 5.5 的证据链仍是 ① 结构体表按名(跨文件/跨遍)→ ② 正在定义类所属
模块 + find_struct_by_module → ③ 本文件已声明类归属表(自引用字段窗口 —— 登记与
查询在同一文件同一遍内,正是该表的作用域)。
This commit is contained in:
TermiNexus committed 2026-09-29 19:14:10 +08:00
1 parent 2b8cc2a69b
commit 1b2324edab
1 file changed
+23 -9
+23 -9
View File
@@ -153,8 +153,18 @@ novirtual class StructGenMixin:
pool: 内存分配器(严格模式下创建 HashTable)
strict: 1=严格模式(用户文件),0=宽松模式(includes 文件)
`ModuleClasses` 也在此**每文件重建**:它的键是 pool 字符串,而 pool 在
文件/遍切换时会被 reset ⇒ 若表跨文件长期存活,`_find_slot` 会 strcmp 到
已回收的键(现场:ASan access-violation,rcx=0x1,栈
mark_module_class → HashTable.set_int → _resize → _find_slot → strcmp)。
与 `VisibleStructs` 同口径(同样每文件新建、键同池),故无悬空。
`CurDefModuleSha1` 指向 `trans.ModuleSha1`(同一 pool 的串),同样必须
每文件清空,否则下一次 strcat/strcmp 读的是已回收内存。
"""
self.StrictVisibility = strict
self.CurDefModuleSha1 = None
self.ModuleClasses = hashtable.HashTable(pool)
if strict != 0:
# 严格模式:创建新的 HashTable 记录可见结构体
self.VisibleStructs = hashtable.HashTable(pool)
@@ -195,18 +205,22 @@ novirtual class StructGenMixin:
def mark_module_class(self, pool: memhub.MemBuddy*, sha1: str, name: str) -> None:
"""登记"{sha1}.{类名}":**该模块声明了这个类**(类定义入口调用一次)。
键里带 SHA1 ⇒ 天然按"声明模块"区分,且与翻译顺序/模块切换无关(不需要
每文件重置):即使表里残留别的模块的登记,查询方只会命中
(它自己那一对 sha1+类名),不会把别的模块的类错认成自己的。
键里带 SHA1 ⇒ 天然按"声明模块"区分。表由 `reset_visible_structs` **每文件
重建**(与键同池 ⇒ 无悬空),因此这里的登记只在本文件本遍内有效 ——
恰好覆盖使用者(`_norm_type_arg_name` 规则 5.5)的窗口:判"类定义中"的
自引用字段时,登记与查询在同一文件同一遍内。
`CurDefModuleSha1` 同步记录("正在定义类所属模块"的权威来源,任何翻译
路径上都是 trans.ModuleSha1)。
"""
if pool is None or sha1 is None or name is None:
if sha1 is None or name is None:
return
# 同步记录"正在定义类所属模块"(任何翻译路径上唯一的权威来源)
self.CurDefModuleSha1 = sha1
if self.ModuleClasses is None:
self.ModuleClasses = hashtable.HashTable(pool)
if self.ModuleClasses is None:
return
if pool is None or self.ModuleClasses is None:
# 未按文件初始化(该路径未调用 reset_visible_structs):不登记。
# 不在此懒建表 —— 表必须与键同池、且随文件重置,否则会 strcmp 到
# 已回收的键(ASan access-violation 现场)。
return
k_buf: str = self._module_class_key(pool, sha1, name)
if k_buf is None:
return