From 41674d125c722f3dcb037b22a2d4b131f180dc14 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Sat, 10 Oct 2026 09:41:15 +0800 Subject: [PATCH] =?UTF-8?q?fix(gen):=20=E4=BF=AE=20vpi=20=E7=94=9F?= =?UTF-8?q?=E6=88=90=E9=98=B6=E6=AE=B5=20is=5Ffile=5Ffunc=20=E8=AF=BB?= =?UTF-8?q?=E5=88=B0=E5=B7=B2=E5=9B=9E=E6=94=B6=E6=B1=A0=E7=9A=84=E6=82=AC?= =?UTF-8?q?=E5=9E=82=E8=A1=A8=EF=BC=88ASan=20=E9=87=8E=E8=AF=BB=E5=B4=A9?= =?UTF-8?q?=E6=BA=83=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 现场:TransPyV.exe(ASan)编译 EscapeOkTest 崩在 Phase A-pre-vpi → _GeneratePyiFromAst → _PyiFunction → compute_escape_summary → _ScanEscapes → _PushInductionEntry → is_file_func → HashTable._slot_hash (access-violation on unknown address 0xffffffffffffffff)。 根因(潜伏已久,非本阶段语义改动引起): - FileClassNames / FileFuncNames / FileVarNames / FileModuleSha1 是按文件重建的表, 表与键共用该文件的瞬时池;契约要求跨文件/跨阶段先清空再重建 (原写在 reset_visible_structs 里那四行)。 - Phase A-pre-vpi 为生成 vpi 会重新 lex/parse 每个文件,但不走 register_file_*、 也不清表 → _PushInductionEntry 的 is_file_func 只认 range 的判据,落在 Phase1 最后一个文件的表上,而那个池已被 reset_to(mark1) 回收。非 ASan 下常表现为读到 0/旧值 的静默错答,ASan 下 0xfd 填充 → 垃圾容量/垃圾 __slots__ → 野读崩溃。 修法: - 抽出 reset_file_name_window() 作为文件级名字窗口的唯一开启入口(原四行清空搬进去, reset_visible_structs 改为调它),并在注释里写死契约。 - 新增轻量 register_file_func_names(pool, tree):只重建本文件顶层函数名(核对过 vpi 路径只需这一张;FileClassNames/FileModuleSha1/FileVarNames 的读取点全在翻译期)。 - Phase A-pre-vpi 循环里,在 _GeneratePyiFromAst 之前调用 reset_file_name_window() + register_file_func_names(mb, tree_vp2)。 - register_file_classes / register_file_funcs 改为无条件重建(原来 没有才建 的写法在 表已被别的阶段建过时会静默复用悬垂表)。 --- App/lib/Phase/Phase2/__init__.vp | 6 +++ App/lib/core/Gen/GenApi.vp | 13 ++++++ App/lib/core/Gen/StructGenMixin.vp | 68 ++++++++++++++++++++++++++---- 3 files changed, 79 insertions(+), 8 deletions(-) diff --git a/App/lib/Phase/Phase2/__init__.vp b/App/lib/Phase/Phase2/__init__.vp index 2134031..7e2b1bf 100644 --- a/App/lib/Phase/Phase2/__init__.vp +++ b/App/lib/Phase/Phase2/__init__.vp @@ -679,6 +679,12 @@ def RunMultiFileProject(mb: memhub.MemBuddy*, source_dir: str, temp_dir: str, ou continue pyi_buf_vp2: bytes = stdlib.malloc(PYI_BUF_SIZE_P2) if pyi_buf_vp2 is not None: + # 文件级名字窗口:本阶段为生成 vpi 重新解析出 tree_vp2,但**不走** register_file_funcs; + # 而 _PyiFunction → compute_escape_summary → _ScanEscapes → _PushInductionEntry 会问 + # is_file_func("range")。不清窗口就会读到"上一个文件的表 + 已被 reset 的池"→ + # 悬垂指针(ASan access-violation / 非 ASan 静默错答)。见 StructGenMixin.reset_file_name_window。 + GenApi.reset_file_name_window() + GenApi.register_file_func_names(mb, tree_vp2) pyi_pos_vp2: sizet = StubConverter._GeneratePyiFromAst(mb, tree_vp2, ent_vp2.Path, pyi_buf_vp2, PYI_BUF_SIZE_P2) if pyi_pos_vp2 > 0: pyi_path_vp2: str = StubMergerCollect._sliced_path(temp_dir, td_len_vpa, ent_vp2.Sha1, "vpi") diff --git a/App/lib/core/Gen/GenApi.vp b/App/lib/core/Gen/GenApi.vp index dddd7be..57185c5 100644 --- a/App/lib/core/Gen/GenApi.vp +++ b/App/lib/core/Gen/GenApi.vp @@ -279,6 +279,19 @@ def register_file_funcs(pool: memhub.MemBuddy*, tree: ast.AST*) -> None: if _g_gen is not None: _g_gen.StructGen.register_file_funcs(pool, tree) +# ---------------- reset_file_name_window / register_file_func_names ---------------- +# "文件级名字窗口"的唯一开启入口 + vpi 落盘路径的轻量重建(契约见 StructGenMixin 两处注释)。 +# 任何文件级入口(翻译 / vpi 落盘 / …)都必须先 reset_file_name_window、再重建对应表。 +def reset_file_name_window() -> None: + """清空四张按文件重建的名字表(FileClassNames/FileModuleSha1/FileFuncNames/FileVarNames)""" + if _g_gen is not None: _g_gen.StructGen.reset_file_name_window() + + +def register_file_func_names(pool: memhub.MemBuddy*, tree: ast.AST*) -> None: + """只重建"本文件顶层函数名"表(供 vpi 落盘路径的 is_file_func 使用;须先 reset_file_name_window)""" + if _g_gen is not None: _g_gen.StructGen.register_file_func_names(pool, tree) + + def is_file_func(name: str) -> int: """Return 1 if name is a top-level function declared in this file, else 0.""" if _g_gen is not None: return _g_gen.StructGen.is_file_func(name) diff --git a/App/lib/core/Gen/StructGenMixin.vp b/App/lib/core/Gen/StructGenMixin.vp index 2a2a9df..6fd8dd7 100644 --- a/App/lib/core/Gen/StructGenMixin.vp +++ b/App/lib/core/Gen/StructGenMixin.vp @@ -236,12 +236,8 @@ novirtual class StructGenMixin: self.StrictVisibility = strict self.CurDefModuleSha1 = None self.CurDefClassName = None - # The file-level class name table is rebuilt per file the same way (keys and table share a pool, so no dangling; re-registered by the Translator pre-scan, see register_file_classes). - # FileModuleSha1 points to trans.ModuleSha1 (same-pool string), so it must be cleared too to avoid reading reclaimed memory in the next file. - self.FileClassNames = None - self.FileModuleSha1 = None - self.FileFuncNames = None - self.FileVarNames = None + # 文件级名字窗口:四张按文件重建的表统一由 reset_file_name_window 清空(契约见该方法的注释) + self.reset_file_name_window() self.ModuleClasses = hashtable.HashTable(pool) if strict != 0: # Strict mode: create a new HashTable recording visible structs. @@ -250,6 +246,26 @@ novirtual class StructGenMixin: # Lenient mode: everything is visible, no HashTable needed. self.VisibleStructs = None + # reset_file_name_window - "文件级名字窗口"的**唯一开启入口**:清空四张按文件重建的表。 + # 契约: + # · FileClassNames / FileFuncNames / FileVarNames 的表与键**共用该文件的瞬时池** + # (AST 名字是指向 src_buf 的零拷贝指针,故键要拷进同一个池),所以跨文件/跨阶段 + # 必须**先清空再重建**; + # · FileModuleSha1 指向 trans.ModuleSha1(同池字符串),同样必须清,否则下一个文件读到被回收的内存。 + # ⚠ 任何"文件级入口"都必须在处理该文件前**先调本方法、再重建对应表**;否则 + # is_file_class / is_file_func / is_file_var 会拿"上一个文件的表 + 已被 reset 的池" + # = 悬垂指针(ASan 下 access-violation;非 ASan 下静默错答)。 + # 现场:Phase A-pre-vpi 只为生成 vpi 重新解析文件、不走 register_file_*,漏开此窗口, + # 于是 _PushInductionEntry → is_file_func("range") 落在 Phase1 已回收的表上, + # HashTable._slot_hash 野读崩溃。 + def reset_file_name_window(self) -> int: + """清空四张按文件重建的名字表(文件级入口必须先调);0=完成""" + self.FileClassNames = None + self.FileModuleSha1 = None + self.FileFuncNames = None + self.FileVarNames = None + return 0 + def add_visible_struct(self, pool: memhub.MemBuddy*, name: str) -> None: """Record a visible struct name (class definition and from-import).""" if name is None: return @@ -307,7 +323,9 @@ novirtual class StructGenMixin: self.FileModuleSha1 = sha1 ch: list[ast.AST*]* = tree.children if ch is None: return - if self.FileClassNames is None: self.FileClassNames = hashtable.HashTable(pool) + # **无条件重建**(不是"没有才建"):重建即"关窗口",任何先前阶段(如 Phase A-pre-vpi) + # 留下的表都会被换掉。若沿用 `if is None` 的旧写法,表非空时会**复用**那张可能已随池回收的表。 + self.FileClassNames = hashtable.HashTable(pool) if self.FileClassNames is None: return cn: sizet = len(ch) ci: sizet @@ -330,12 +348,46 @@ novirtual class StructGenMixin: if name is None or self.FileClassNames is None: return 0 return self.FileClassNames.__contains__(name) + # register_file_func_names - **只**重建"本文件顶层函数名"集合(vpi 落盘路径专用)。 + # Phase A-pre-vpi 会为每个文件重新 lex/parse 出独立 AST 再生成 vpi,但不走 register_file_funcs; + # 而 compute_escape_summary 的归纳变量判据要问 is_file_func("range") + # ("本文件自己定义过 range 就放弃区间推断",见 _PushInductionEntry)。 + # 核对过消费者:vpi 路径**只需要这一张表**——FileClassNames / FileModuleSha1 / FileVarNames 的 + # 读取点(TypeGenResolve 的类型归属 / AssignGenMixin 的全局写判据)全部在翻译期; + # 另一个 is_file_func 调用点(VpiReader 注入自家 declare 的守卫)标注为 full-translation only。 + # 调用方必须**先 reset_file_name_window**(本方法只负责"重建"那一半),否则会与既有表叠加。 + def register_file_func_names(self, pool: memhub.MemBuddy*, tree: ast.AST*) -> None: + """重建"本文件顶层函数名"表(供 vpi 落盘路径的 is_file_func 使用)""" + if pool is None or tree is None: return + ch: list[ast.AST*]* = tree.children + if ch is None: return + self.FileFuncNames = hashtable.HashTable(pool) + if self.FileFuncNames is None: return + cn: sizet = len(ch) + ci: sizet = 0 + while ci < cn: + child: ast.AST* = ch.get(ci) + ci += 1 + if child is None or child.kind() != ast.ASTKind.FunctionDef: continue + fd: ast.FunctionDef* = ast.FunctionDef*(child) + if fd is None or fd.name is None: continue + if self.FileFuncNames.__contains__(fd.name) != 0: continue + # 键必须与表同池(AST 名字指向 src_buf 的零拷贝指针,跨阶段可能失效)——同 register_file_classes + nm_len: sizet = string.strlen(fd.name) + nm_buf: str = pool.alloc(nm_len + 1) + if nm_buf is None: continue + string.strcpy(nm_buf, fd.name) + self.FileFuncNames.set_int(nm_buf, 1) + def register_file_funcs(self, pool: memhub.MemBuddy*, tree: ast.AST*) -> None: """Pre-register the top-level function names of this file.""" if pool is None or tree is None: return ch: list[ast.AST*]* = tree.children if ch is None: return - if self.FileFuncNames is None: self.FileFuncNames = hashtable.HashTable(pool) + # **无条件重建**:同 register_file_classes——重建即"关窗口",避免复用上一阶段/上一文件 + # 留下的、其池可能已被 reset 的表(旧写法 `if is None` 在这种情形下会静默复用悬垂表)。 + self.FileFuncNames = hashtable.HashTable(pool) + self.FileVarNames = None # 本函数的后续循环里按需重建(每文件一次) if self.FileFuncNames is None: return # 生命期摘要表:**只分配一次**(PermPool,对照 init_struct_table 的长生命周期表口径), # 此后每文件仅重置 EscCount。表内只存 u64 名哈希,且读只发生在 [0, EscCount) 区间;