fix(gen): P2 失效状态改存独立槽表,不再给 VarEntry 加字段

现象:任何工程编译都在函数形参注册刚结束处 access-violation(pc=0,逐字节可复现)。定位:逐层探针把崩点压到 FuncGenDef 的 pre_scan 段,而该段对 Check(body 仅 if+return)应为空操作,唯一变量是 VarEntry 这个热结构被加了 Freed 字段。修法:撤销 VarEntry.Freed,改用 VarGenMixin 的独立失效槽表 _g_freed_slots(键 = 变量槽 alloca,整次翻译内唯一,故全局累积、无需按函数 reset/restore);_FreedMark/_FreedCheck/_FreedClearSlot 三个 API 替换字段读写,P2 语义不变。同时清除全部 TEMP 探针。
This commit is contained in:
TermiNexus committed 2026-10-09 11:51:11 +08:00
1 parent 95b076581a
commit dd24dc670f
10 files changed
+58 -47

No files matched your search

-2
View File
@@ -840,8 +840,6 @@ def RunMultiFileProject(mb: memhub.MemBuddy*, source_dir: str, temp_dir: str, ou
done_flags[i] = 1 done_flags[i] = 1
completed_this_pass += 1 completed_this_pass += 1
continue continue
# [TEMP dbg] 阶段日志:定位 Phase A 崩溃点(定位后移除)
VLogger.info(f"PhaseA translating: {ent.Path}", "PhaseA")
# Compute the package name (the directory part relative to source_dir) # Compute the package name (the directory part relative to source_dir)
src_dir_len_pa: sizet = string.strlen(source_dir) src_dir_len_pa: sizet = string.strlen(source_dir)
pkg_pa: str = None pkg_pa: str = None
-2
View File
@@ -661,8 +661,6 @@ def _PyiClass(buf: bytes, buf_size: sizet, pos: sizet, cls: ast.ClassDef*, inden
# Returns bytes written (0 on failure); rel_path should be absolute for error attribution. # Returns bytes written (0 on failure); rel_path should be absolute for error attribution.
def _GeneratePyiFromAst(mb: memhub.MemBuddy*, tree: ast.AST*, rel_path: str, buf: bytes, buf_size: sizet) -> sizet: def _GeneratePyiFromAst(mb: memhub.MemBuddy*, tree: ast.AST*, rel_path: str, buf: bytes, buf_size: sizet) -> sizet:
"""Generate .vpi stub content from an AST into buf""" """Generate .vpi stub content from an AST into buf"""
# [TEMP dbg] 阶段日志:进入 vpi 生成(定位崩溃点,定位后移除)
VLogger.info(f"vpi.gen: {rel_path}", "StubGen")
if tree is None or buf is None or buf_size == 0: return 0 if tree is None or buf is None or buf_size == 0: return 0
# Error attribution: point "current file" at the file being stubbed. Callers must pass # Error attribution: point "current file" at the file being stubbed. Callers must pass
+1 -4
View File
@@ -535,10 +535,7 @@ novirtual class CallGenCall(CallGenBase.CallGenBase):
if arg_node.kind() != ast.ASTKind.Name: return 0 if arg_node.kind() != ast.ASTKind.Name: return 0
nm: ast.Name* = ast.Name*(arg_node) nm: ast.Name* = ast.Name*(arg_node)
if nm.id is None: return 0 if nm.id is None: return 0
ent: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm.id) return self.Gen.VarGen._FreedMark(self.Gen.VarGen.lookup_var(trans.SymTab, nm.id))
if ent is None: return 0
ent.Freed = 1
return 1
# _translate_method_call - translate obj.method(args) into a call with the # _translate_method_call - translate obj.method(args) into a call with the
# object pointer (obj_ptr, an alloca of Ptr(StructTy)) as the self argument. # object pointer (obj_ptr, an alloca of Ptr(StructTy)) as the self argument.
+1 -2
View File
@@ -42,8 +42,7 @@ novirtual class ExprGenName(ExprGenBase.ExprGenBase):
# P2 willFree:读取已被"消费"的变量(其所有权被 @life(willFree(...)) 标注的调用夺取)→ fail-fast。 # P2 willFree:读取已被"消费"的变量(其所有权被 @life(willFree(...)) 标注的调用夺取)→ fail-fast。
# 只在**读**路径检查;对同名变量重新赋值会清除失效态(见 AssignGenMixin)。 # 只在**读**路径检查;对同名变量重新赋值会清除失效态(见 AssignGenMixin)。
if trans is not None: if trans is not None:
ent_rd: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm_id) if self.Gen.VarGen._FreedCheck(self.Gen.VarGen.lookup_var(trans.SymTab, nm_id)) != 0:
if ent_rd is not None and ent_rd.Freed != 0:
self.Gen.TypeGenErr.fatal_error(node, f"cannot use '{nm_id}': its value was released by a call annotated with @life(willFree(...)) and is no longer valid", "LifetimeError") self.Gen.TypeGenErr.fatal_error(node, f"cannot use '{nm_id}': its value was released by a call annotated with @life(willFree(...)) and is no longer valid", "LifetimeError")
# CDefine compile-time constant: return the constant (float -> double, int -> i32). # CDefine compile-time constant: return the constant (float -> double, int -> i32).
# Existing variables win over CDefine table entries injected from vpi stubs (lookup_var guard). # Existing variables win over CDefine table entries injected from vpi stubs (lookup_var guard).
+1 -2
View File
@@ -585,8 +585,7 @@ novirtual class ExprGenOps(ExprGenBase.ExprGenBase):
nm_ao: ast.Name* = ast.Name*(uo.operand) nm_ao: ast.Name* = ast.Name*(uo.operand)
if nm_ao.id is not None: if nm_ao.id is not None:
# P2 willFree:对已失效变量取地址 → fail-fast(与读取路径同一口径) # P2 willFree:对已失效变量取地址 → fail-fast(与读取路径同一口径)
ve_ao: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm_ao.id) if self.Gen.VarGen._FreedCheck(self.Gen.VarGen.lookup_var(trans.SymTab, nm_ao.id)) != 0:
if ve_ao is not None and ve_ao.Freed != 0:
self.Gen.TypeGenErr.fatal_error(node, f"cannot take the address of '{nm_ao.id}': its value was released by a call annotated with @life(willFree(...)) and is no longer valid", "LifetimeError") self.Gen.TypeGenErr.fatal_error(node, f"cannot take the address of '{nm_ao.id}': its value was released by a call annotated with @life(willFree(...)) and is no longer valid", "LifetimeError")
# self/cls object reference: return the object pointer itself (Ptr(Struct)), not the pointer variable's address (Ptr(Ptr(Struct))). # self/cls object reference: return the object pointer itself (Ptr(Struct)), not the pointer variable's address (Ptr(Ptr(Struct))).
if string.strcmp(nm_ao.id, "self") == 0 or string.strcmp(nm_ao.id, "cls") == 0: if string.strcmp(nm_ao.id, "self") == 0 or string.strcmp(nm_ao.id, "cls") == 0:
-8
View File
@@ -255,8 +255,6 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase):
"""Translate a function definition; returns the number of added variables (usually 0).""" """Translate a function definition; returns the number of added variables (usually 0)."""
fd: ast.FunctionDef* = ast.FunctionDef*(node) fd: ast.FunctionDef* = ast.FunctionDef*(node)
if fd is None or fd.name is None: return 0 if fd is None or fd.name is None: return 0
# [TEMP dbg] 阶段日志:定位崩溃到具体函数(定位后移除)
VLogger.info(f"TR.fn: {fd.name}", "TR")
# PEP 695 generic function templates are not emitted directly (same rule as forward_declare_functions): # PEP 695 generic function templates are not emitted directly (same rule as forward_declare_functions):
# clones are emitted at call sites by _specialize_generic_function; here we only ensure the template is registered. # clones are emitted at call sites by _specialize_generic_function; here we only ensure the template is registered.
@@ -386,7 +384,6 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase):
# Record value-reference (&T / &temp T) parameter modes for call-site auto-address. # Record value-reference (&T / &temp T) parameter modes for call-site auto-address.
self._register_func_auto_addr_modes(trans, mangled_name, fd) self._register_func_auto_addr_modes(trans, mangled_name, fd)
VLogger.info("TR.p1.auto_addr.done", "TR")
# Register the CExport function in the global table (for cross-module calls). t.CExport definitions use bare # Register the CExport function in the global table (for cross-module calls). t.CExport definitions use bare
# names (@strlen); cross-module calls consult the table to use the bare name instead of @{sha1}.func. # names (@strlen); cross-module calls consult the table to use the bare name instead of @{sha1}.func.
@@ -761,7 +758,6 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase):
# Reset the in-chain nonlocal heap prebuild flag (built once per function; multiple nested function definitions # Reset the in-chain nonlocal heap prebuild flag (built once per function; multiple nested function definitions
# share one heap, preventing repeated malloc + double VarEntry redirection). # share one heap, preventing repeated malloc + double VarEntry redirection).
trans._nl_chain_done = 0 trans._nl_chain_done = 0
VLogger.info("TR.p1c.state.saved", "TR")
# Pre-scan the body: 1) Global/Nonlocal declarations first (Python semantics: they apply to the whole function # Pre-scan the body: 1) Global/Nonlocal declarations first (Python semantics: they apply to the whole function
# scope regardless of position, so register before any assignment); 2) AnnAssign to create local allocas early. # scope regardless of position, so register before any assignment); 2) AnnAssign to create local allocas early.
@@ -775,17 +771,13 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase):
stmt: ast.AST* = body.get(bi) stmt: ast.AST* = body.get(bi)
if stmt is not None: self.Gen.StmtGenDecl.pre_scan_allocas(trans, stmt) if stmt is not None: self.Gen.StmtGenDecl.pre_scan_allocas(trans, stmt)
VLogger.info("TR.p1d.prescan.done", "TR")
# M3 owned slife(§4.2 函数级 scope guard):进入函数体前把登记区间起点抬到当前栈顶。 # M3 owned slife(§4.2 函数级 scope guard):进入函数体前把登记区间起点抬到当前栈顶。
# 表是函数间共享的一段栈式区间,嵌套函数各自独立;退出时由 _SlifeRestore 弹回外层起点。 # 表是函数间共享的一段栈式区间,嵌套函数各自独立;退出时由 _SlifeRestore 弹回外层起点。
# 放在这里(而非更早)是因为上方存在多条提前返回分支(泛型模板/async/extern), # 放在这里(而非更早)是因为上方存在多条提前返回分支(泛型模板/async/extern),
# 只有从这里开始才保证"有 reset 必有 restore"。 # 只有从这里开始才保证"有 reset 必有 restore"。
old_slife_base: int = self.Gen.VarGen._SlifeReset(pool) old_slife_base: int = self.Gen.VarGen._SlifeReset(pool)
VLogger.info("TR.p2.slife_reset.done", "TR")
# Translate the function body # Translate the function body
VLogger.info("TR.p3.body.begin", "TR")
if body is not None: if body is not None:
bn2: sizet = len(body) bn2: sizet = len(body)
for bi2 in range(bn2): for bi2 in range(bn2):
+3 -3
View File
@@ -79,9 +79,9 @@ class VarEntry:
# 生命期标签(LifeTag,见文件头 define LIFE_*):该变量**当前值**的生命期。 # 生命期标签(LifeTag,见文件头 define LIFE_*):该变量**当前值**的生命期。
# 追加在末尾以保持既有字段偏移不变。默认 LIFE_UNKNOWN(未登记 = 保守放行)。 # 追加在末尾以保持既有字段偏移不变。默认 LIFE_UNKNOWN(未登记 = 保守放行)。
Life: int Life: int
# P2 willFree:1=该变量已被某个带 @life(willFree(...)) 标注的调用"消费",其后再读/取址/传参 # 注:P2 的"已失效"状态**不放在这里** —— 给这个热结构加字段会改变布局、牵连整条变量注册
# 即 LifetimeError(fail-fast)。追加在末尾以保持既有字段偏移不变;add_var 显式置 0。 # 路径(现场:任何工程编译时都在"函数形参注册刚结束"处 access-violation)。改由
Freed: int # VarGenMixin 的独立失效槽表 _g_freed_slots 保存(键 = 变量槽 alloca,槽本身唯一)。
# ============================================================ # ============================================================
# Scope - scope node (formerly HandlesVar.Scope) # Scope - scope node (formerly HandlesVar.Scope)
-2
View File
@@ -27,8 +27,6 @@ novirtual class StmtGenMixin(StmtGenDeclMixin.StmtGenDeclMixin):
def translate_stmt(self, trans: HT.Translator*, node: ast.AST*) -> int: def translate_stmt(self, trans: HT.Translator*, node: ast.AST*) -> int:
"""Translate a single statement with a depth guard; returns the number of new variables""" """Translate a single statement with a depth guard; returns the number of new variables"""
# [TEMP dbg] 阶段日志:语句级(定位崩溃到具体语句,定位后移除)
if node is not None: VLogger.info(f"stmt.kind={node.kind()}", "TR")
if trans is not None: if trans is not None:
# --traceback: statement-level DWARF location (no-op outside a DI-attached # --traceback: statement-level DWARF location (no-op outside a DI-attached
# define). Every instruction then carries ", !dbg !N". # define). Every instruction then carries ", !dbg !N".
-4
View File
@@ -381,8 +381,6 @@ novirtual class Translator:
"""Translate the AST into LLVM IR.""" """Translate the AST into LLVM IR."""
if tree is None: return 1 if tree is None: return 1
if _mbuddy is None: return 1 if _mbuddy is None: return 1
# [TEMP dbg] 阶段日志:定位崩溃点(定位后移除)
VLogger.info(f"TR.enter sha1={self.ModuleSha1}", "TR")
pool: memhub.MemBuddy* = _mbuddy pool: memhub.MemBuddy* = _mbuddy
@@ -455,8 +453,6 @@ novirtual class Translator:
# (现场:Test/NewLifetimeTest 未 import stdlib,拿不到 stdlib vpi 的 declare)。 # (现场:Test/NewLifetimeTest 未 import stdlib,拿不到 stdlib vpi 的 declare)。
free_func: mlirlite.Function* = mlirlite.create_declare(decl_pool, mod, "free", void_ty_decl) free_func: mlirlite.Function* = mlirlite.create_declare(decl_pool, mod, "free", void_ty_decl)
if free_func is not None: mlirlite.add_param(decl_pool, free_func, i8_ptr_ty, "p") if free_func is not None: mlirlite.add_param(decl_pool, free_func, i8_ptr_ty, "p")
# [TEMP dbg] 阶段日志:模块前言完成(定位崩溃点,定位后移除)
VLogger.info("TR.prelude.done", "TR")
# Check whether the user defined a main function # Check whether the user defined a main function
has_user_main: int = 0 has_user_main: int = 0
+52 -18
View File
@@ -44,6 +44,12 @@ define SCOPE_BLOCK = 2
# 0 号槽覆写外层登记项,且内层出口会把外层的对象一起 free 掉。 # 0 号槽覆写外层登记项,且内层出口会把外层的对象一起 free 掉。
# 登记表容量(P5:128 → 256)。超限时 **fail-fast**(不再静默退化为"不登记 = 不释放"的泄漏)。 # 登记表容量(P5:128 → 256)。超限时 **fail-fast**(不再静默退化为"不登记 = 不释放"的泄漏)。
define _SLIFE_MAX = 256 define _SLIFE_MAX = 256
# P2 willFree 失效槽表:_g_freed_slots[0.._g_freed_count) 是"已被标注调用消费过的变量槽 alloca"。
# **刻意不放进 VarEntry**(给热结构加字段会动布局、牵连整条变量注册路径)。
# 键 = alloca,槽在整次翻译中唯一,所以是"全局累积表",无需按函数 reset/restore。
define _FREED_MAX = 4096
_g_freed_slots: sizet* = None
_g_freed_count: int = 0
_g_slife_owned: sizet* = None _g_slife_owned: sizet* = None
_g_slife_count: int = 0 _g_slife_count: int = 0
_g_slife_top: int = 0 _g_slife_top: int = 0
@@ -288,41 +294,69 @@ novirtual class VarGenMixin:
"""Add a variable to the variable table""" """Add a variable to the variable table"""
if name is None or alloca is None or vars_ptr is None: return 1 if name is None or alloca is None or vars_ptr is None: return 1
if var_count >= HT.MAX_VARS: return 1 if var_count >= HT.MAX_VARS: return 1
VLogger.info("dbg.a1.enter", "TR")
entry_size: sizet = GenTypes.VarEntry.__sizeof__() entry_size: sizet = GenTypes.VarEntry.__sizeof__()
entry_addr: uint64 = uint64(vars_ptr) + var_count * entry_size entry_addr: uint64 = uint64(vars_ptr) + var_count * entry_size
entry: GenTypes.VarEntry* = GenTypes.VarEntry*(entry_addr) entry: GenTypes.VarEntry* = GenTypes.VarEntry*(entry_addr)
VLogger.info(f"dbg.a2.entry esz={entry_size} cnt={var_count}", "TR") # Deep-copy the variable name into stdlib (survives pool reset): storing the AST Name.id pointer lets
# later allocations overwrite it, so find_var's strcmp silently misses and rebuilds a degenerate alloca.
name_len: sizet = string.strlen(name) name_len: sizet = string.strlen(name)
VLogger.info(f"dbg.a3.namelen={name_len}", "TR")
name_copy: bytes = stdlib.malloc(name_len + 1) name_copy: bytes = stdlib.malloc(name_len + 1)
if name_copy is None: return 1 if name_copy is None: return 1
string.strcpy(name_copy, name) string.strcpy(name_copy, name)
VLogger.info("dbg.a4.copied", "TR")
entry.Name = name_copy entry.Name = name_copy
VLogger.info("dbg.a5.name", "TR")
entry.Alloca = alloca entry.Alloca = alloca
entry.Used = 1 entry.Used = 1
# Explicitly zero the const flag (do not rely on the entry's zero initialization): set_var_const
# sets it to 1 only for real const objects, so zeroing avoids false positives.
entry.IsConst = 0 entry.IsConst = 0
VLogger.info("dbg.a6.const", "TR") # 生命期标签显式初始化:未登记 = LIFE_UNKNOWN(保守放行)。
entry.Life = GenTypes.LIFE_UNKNOWN entry.Life = GenTypes.LIFE_UNKNOWN
VLogger.info("dbg.a7.life", "TR")
# P2 willFree 的 entry.Freed **不在这里写**:init_vars 已对整块 memset 清零(见 init_vars),
# 逐个字段写末端偏移在"结构体刚加字段"时容易踩到;统一依赖整块清零。
# [S3 dual write] Value-type Spec (alloca.Ty is Ptr(value type), so take its pointee) # [S3 dual write] Value-type Spec (alloca.Ty is Ptr(value type), so take its pointee)
entry.Spec = self._spec_of_var_alloca(alloca) entry.Spec = self._spec_of_var_alloca(alloca)
VLogger.info("dbg.a8.spec", "TR") return 0
# ---- P2 willFree 失效槽表 API(不改 VarEntry 布局) ----
def _FreedMark(self, slot: mlirlite.Value*) -> int:
"""把变量槽标为"已失效"(被 @life(willFree(...)) 标注的调用消费过);1=已标记 / 0=未处理"""
global _g_freed_slots, _g_freed_count
if slot is None: return 0
if self._FreedCheck(slot) != 0: return 1
if _g_freed_slots is None:
_g_freed_slots = sizet*(stdlib.malloc(_FREED_MAX * 8))
if _g_freed_slots is None: return 0
if _g_freed_count >= _FREED_MAX:
self.Gen.TypeGenErr.fatal_error(None, "the willFree 'released variable' table is full (capacity 4096): too many released variables in one translation", "LifetimeError")
return 0
_g_freed_slots[_g_freed_count] = sizet(slot)
_g_freed_count += 1
return 1
def _FreedCheck(self, slot: mlirlite.Value*) -> int:
"""该变量槽是否已被标为失效;1=已失效(其后再读/取址/传参即 LifetimeError)/ 0=有效"""
if slot is None or _g_freed_slots is None: return 0
i: int = 0
while i < _g_freed_count:
if uint64(_g_freed_slots[i]) == uint64(slot): return 1
i += 1
return 0
def _FreedClearSlot(self, slot: mlirlite.Value*) -> int:
"""清除某槽的失效态(对同名变量重新赋值后调用);1=清除过 / 0=本来不在表里"""
global _g_freed_count
if slot is None or _g_freed_slots is None: return 0
i: int = 0
while i < _g_freed_count:
if uint64(_g_freed_slots[i]) == uint64(slot):
_g_freed_count -= 1
_g_freed_slots[i] = _g_freed_slots[_g_freed_count]
return 1
i += 1
return 0 return 0
def clear_var_freed(self, symtab: GenTypes.SymbolTable*, name: str) -> int: def clear_var_freed(self, symtab: GenTypes.SymbolTable*, name: str) -> int:
"""P2 willFree:对同名变量重新赋值后清除失效态;1=清除过 / 0=无该项 """P2 willFree:对同名变量重新赋值后清除失效态;1=清除过 / 0=无该项"""
if symtab is None or name is None: return 0
变量已存在时赋值走 get_or_create_sym(不重建 VarEntry),所以失效态必须显式清除—— return self._FreedClearSlot(self.lookup_var(symtab, name))
否则 `p = malloc(8); free(p); p = malloc(16); Use(p)` 会被误报。"""
e: GenTypes.VarEntry* = self.lookup_var_entry(symtab, name)
if e is None: return 0
e.Freed = 0
return 1
# _spec_of_var_alloca - build a TypeSpec from the alloca's value type (S3 dual write). # _spec_of_var_alloca - build a TypeSpec from the alloca's value type (S3 dual write).
# alloca.Ty is Ptr(var_ty), so the pointee is the value type; None means failure or an unsupported type. # alloca.Ty is Ptr(var_ty), so the pointee is the value type; None means failure or an unsupported type.