From 2353dca8ec6fc83765f964d54a1db0c7eba29b3b Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Fri, 9 Oct 2026 19:32:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(diag):=20=E9=80=83=E9=80=B8=E7=B1=BB=20Lif?= =?UTF-8?q?etimeError=20=E8=BF=BD=E5=8A=A0=20File=20lifetime=20leak=20?= =?UTF-8?q?=E5=B8=A7=EF=BC=8C=E6=A0=87=E5=87=BA=E6=B3=84=E6=BC=8F=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit StructGenMixin: 新增 escape_leak_line——错误路径重扫被调者函数体,取指定形参的**首个逃逸点**行号; _NoteLeak 在三处置位来源采集(字段/全局写、return 形参、传递闭包调用),新增 EscLeakWant/EscLeakLine。 Diag: DiagEntry 增加 LeakFileName/LeakLine + set_leak + emit_leak_frame(标签 File lifetime leak), 打印顺序 = 调用链 → 错误点 → 泄漏点 → 类别: 消息。 TypeGenErr.fatal_error / GenApi.fail_if_stack_addr_escape 增加 leak_file/leak_line 透传; CallGenMixin 调用点按失败实参位取行号。**仅同模块**:跨模块 EscFuncTable 无 AST → 返回 0,不打该帧。 --- App/lib/core/Diag.vp | 32 ++++++++++++++++ App/lib/core/Gen/AssignGenMixin.vp | 8 +++- App/lib/core/Gen/Call/CallGenMixin.vp | 4 +- App/lib/core/Gen/GenApi.vp | 17 +++++++-- App/lib/core/Gen/StructGenMixin.vp | 54 ++++++++++++++++++++++++--- App/lib/core/Gen/Type/TypeGenErr.vp | 10 ++++- 6 files changed, 112 insertions(+), 13 deletions(-) diff --git a/App/lib/core/Diag.vp b/App/lib/core/Diag.vp index 9e0e307..3ab8d7f 100644 --- a/App/lib/core/Diag.vp +++ b/App/lib/core/Diag.vp @@ -54,6 +54,10 @@ class DiagEntry: FrameCount: int # 调用链帧数 FrameFiles: sizet* # 调用链每帧文件名(深拷贝) FrameLines: sizet* # 调用链每帧行号(与 FrameFiles 同容量) + # 泄漏点(可选):被调者那边**造成逃逸的写入点**。只有逃逸类 LifetimeError 会有值; + # 打印成独立一帧 `File lifetime leak "", line N`,与调用栈帧(`File "..."`)区分。 + LeakFileName: str + LeakLine: int Cap: int # 内部:FrameFiles/FrameLines 容量(不参与打印) @@ -81,6 +85,8 @@ def new_entry(max_frames: int) -> DiagEntry*: ent.Msg = None ent.Hint = None ent.FrameCount = 0 + ent.LeakFileName = None + ent.LeakLine = 0 ent.Cap = max_frames ent.FrameFiles = (sizet*)(stdlib.malloc(sizet(max_frames) * 8)) ent.FrameLines = (sizet*)(stdlib.malloc(sizet(max_frames) * 8)) @@ -98,6 +104,8 @@ def begin(ent: DiagEntry*, category: str, msg: str, hint: str = None) -> int: ent.ColStart = 0 ent.ColEnd = 0 ent.FrameCount = 0 + ent.LeakFileName = None + ent.LeakLine = 0 return 0 @@ -111,6 +119,14 @@ def set_current(ent: DiagEntry*, file_name: str, lineno: int, col_start: int, co return 0 +# set_leak - 设置"泄漏点"帧(File lifetime leak);仅逃逸类 LifetimeError 使用 +def set_leak(ent: DiagEntry*, file_name: str, lineno: int) -> int: + if ent is None or lineno <= 0: return 0 + ent.LeakFileName = dup_str(file_name) + ent.LeakLine = lineno + return 0 + + # add_frame - 追加调用链一帧(按"自外向内"顺序追加) def add_frame(ent: DiagEntry*, file_name: str, lineno: int) -> int: if ent is None or ent.FrameFiles is None or ent.FrameLines is None: return 0 @@ -137,6 +153,8 @@ def emit(ent: DiagEntry*, direction: int) -> int: emit_frame(str(ent.FrameFiles[i_u]), int(ent.FrameLines[i_u])) i_u += 1 emit_current(ent.FileName, ent.Line, ent.ColStart, ent.ColEnd) + # 泄漏点帧紧跟在"错误点"之后(往下再一行),再印类别: 消息 + if ent.LeakLine > 0: emit_leak_frame(ent.LeakFileName, ent.LeakLine) emit_category(ent.Category, ent.Msg) return 0 @@ -145,6 +163,7 @@ def emit(ent: DiagEntry*, direction: int) -> int: def free_entry(ent: DiagEntry*) -> int: if ent is None: return 0 if ent.FileName is not None: stdlib.free(ent.FileName) + if ent.LeakFileName is not None: stdlib.free(ent.LeakFileName) if ent.Category is not None: stdlib.free(ent.Category) if ent.Msg is not None: stdlib.free(ent.Msg) if ent.Hint is not None: stdlib.free(ent.Hint) @@ -222,6 +241,19 @@ def emit_frame(file_name: str, lineno: int) -> int: return 0 +# emit_leak_frame - "泄漏点"帧:标签用 `File lifetime leak`,与调用栈帧(`File "..."`)区分 +def emit_leak_frame(file_name: str, lineno: int) -> int: + if file_name is None: return 0 + if lineno > 0: + stdio.printf(' File lifetime leak "%s", line %d\n', file_name, lineno) + src: str = read_source_line(file_name, lineno) + if src is not None: + stdio.printf(" %s\n", src) + stdlib.free(src) + else: stdio.printf(' File lifetime leak "%s"\n', file_name) + return 0 + + # emit_current - 当前帧(带 caret 列范围;行读不出时印警告) def emit_current(file_name: str, lineno: int, col_start: int, col_end: int) -> int: # Sanity defense: read the line first to verify lineno belongs to the current file. diff --git a/App/lib/core/Gen/AssignGenMixin.vp b/App/lib/core/Gen/AssignGenMixin.vp index aefdf44..0fd0cf8 100644 --- a/App/lib/core/Gen/AssignGenMixin.vp +++ b/App/lib/core/Gen/AssignGenMixin.vp @@ -716,7 +716,7 @@ novirtual class AssignGenMixin: # fail_if_life_escapes - 汇点检查:被写出表达式的生命期为 LIFE_FRAME → LifetimeError。 # 这是 GenApi.fail_if_stack_addr_escape 的唯一实现(语法判据已并入 life_of_expr)。 - def fail_if_life_escapes(self, node: ast.AST*, where: str, trans: HT.Translator*, target_ty: mlirlite.LLVMType*) -> int: + def fail_if_life_escapes(self, node: ast.AST*, where: str, trans: HT.Translator*, target_ty: mlirlite.LLVMType*, leak_line: int = 0) -> int: """Fail fast (LifetimeError) when the written-out value is frame-lifetime storage""" if node is None or trans is None: return 0 if self.life_of_expr(trans, node, target_ty) != GenTypes.LIFE_FRAME: return 0 @@ -733,7 +733,11 @@ novirtual class AssignGenMixin: if n3.id is not None: nm_txt = n3.id nm_pre = "&" - self.Gen.TypeGenErr.fatal_error(node, f"stack address escapes: `{nm_pre}{nm_txt}` is frame-local; using it at {where} hands out a dangling pointer once this frame returns. Copy the value, or allocate on the heap (`new heap hlife ...`).", "LifetimeError") + # 泄漏点(仅同模块调用点会传 leak_line>0):被调者那边**造成逃逸的写入点**在同一个源文件里, + # 追加一帧 "File lifetime leak ..." 指出它,读者才知道值是从哪一行漏出去的。 + leak_f: str = None + if leak_line > 0: leak_f = self.Gen.TypeGenResolve.get_current_file() + self.Gen.TypeGenErr.fatal_error(node, f"stack address escapes: `{nm_pre}{nm_txt}` is frame-local; using it at {where} hands out a dangling pointer once this frame returns. Copy the value, or allocate on the heap (`new heap hlife ...`).", "LifetimeError", 0, 0, leak_f, leak_line) return 0 # _SlifeDropOwned - 该槽已被登记为 slife 绑定时交还所有权:释放它当前拥有的对象、影子清 0。 diff --git a/App/lib/core/Gen/Call/CallGenMixin.vp b/App/lib/core/Gen/Call/CallGenMixin.vp index 4dc86db..baa6711 100644 --- a/App/lib/core/Gen/Call/CallGenMixin.vp +++ b/App/lib/core/Gen/Call/CallGenMixin.vp @@ -196,7 +196,9 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase): _dummy_ptr: mlirlite.LLVMType* = mlirlite.Ptr(pool, mlirlite.Int8(pool)) for ai in range(can): if ((esc_mask >> int(ai)) & 1) != 0: - GenApi.fail_if_stack_addr_escape(cargs.get(ai), "an escaping callee parameter", trans, _dummy_ptr) + # 泄漏点(**仅同模块**:EscFuncTable 按文件重置,跨模块返回 0 → 不打该帧) + leak_ln: int = GenApi.escape_leak_line(func_name, int(ai)) + GenApi.fail_if_stack_addr_escape(cargs.get(ai), "an escaping callee parameter", trans, _dummy_ptr, leak_ln) # P2 willFree:查被调者的形参失效标注(本文件表 → 跨模块 .vpi 回退)。 # 标记动作在**实参全部翻译完成之后**(见下方 "# Default-argument filling" 之前), # 否则 `stdlib.free(p)` 自己那次读取 p 会被误判为"使用已失效变量"。 diff --git a/App/lib/core/Gen/GenApi.vp b/App/lib/core/Gen/GenApi.vp index e758004..bcd26a5 100644 --- a/App/lib/core/Gen/GenApi.vp +++ b/App/lib/core/Gen/GenApi.vp @@ -497,6 +497,14 @@ def get_param_escapes_hard_method(pool: memhub.MemBuddy*, class_name: str, metho return 0 +# escape_leak_line - **错误路径专用**:被调者第 want_pi 号形参的"首个逃逸写入点"行号。 +# 只在 fail-fast 报错时调用(会重扫一次被调者函数体);跨模块拿不到 AST 时返回 0。 +def escape_leak_line(name: str, want_pi: int) -> int: + """形参 want_pi 的逃逸点行号;0 = 未知(含跨模块)""" + if _g_gen is not None: return _g_gen.StructGen.escape_leak_line(name, want_pi) + return 0 + + def get_param_willfree(name: str) -> int: """本文件函数的 willFree 形参掩码(第 k 位=第 k 个形参在本调用后失效);无记录返回 0""" if _g_gen is not None: return _g_gen.StructGen.get_param_willfree(name) @@ -725,10 +733,13 @@ def escape_fstring_copy(src: str) -> str: # (b) `name` — a bare local ARRAY used where a pointer is expected (array decay); # (c) propagated borrows — `s: str = l` gives Life(s)=FRAME, so `return s` is caught. # A bare name with a matching aggregate target is a value copy, never an escape. -def fail_if_stack_addr_escape(node: ast.AST*, where: str, trans: HT.Translator*, target_ty: mlirlite.LLVMType* = None) -> int: - """Fail fast (LifetimeError) when the address of a stack object escapes the frame.""" +def fail_if_stack_addr_escape(node: ast.AST*, where: str, trans: HT.Translator*, target_ty: mlirlite.LLVMType* = None, leak_line: int = 0) -> int: + """Fail fast (LifetimeError) when the address of a stack object escapes the frame. + + leak_line > 0 时附带"泄漏点"帧(被调者那边造成逃逸的写入点行号,见 escape_leak_line)。 + """ if node is None or trans is None or _g_gen is None: return 0 - return _g_gen.AssignGen.fail_if_life_escapes(node, where, trans, target_ty) + return _g_gen.AssignGen.fail_if_life_escapes(node, where, trans, target_ty, leak_line) diff --git a/App/lib/core/Gen/StructGenMixin.vp b/App/lib/core/Gen/StructGenMixin.vp index 8b2723f..a36633d 100644 --- a/App/lib/core/Gen/StructGenMixin.vp +++ b/App/lib/core/Gen/StructGenMixin.vp @@ -168,7 +168,12 @@ novirtual class StructGenMixin: # "硬逃逸位"表(键口径同 EscTable):该位由**全局写 / 返回形参 / 传递闭包 / base 未定的字段写**造成。 # 与"只写进形参对象字段"(调用点可判同命 → 放行)区分开:字段位 = 逃逸位 & ~硬位。每文件重置。 EscFieldTable: sizet* - EscFieldCount: int + EscFieldCount: int + # ── 逃逸点采集(**只走错误路径**)── + # escape_leak_line 重扫被调者函数体前,把"想要的形参位"放进 EscLeakWant;_MarkEscapingStore + # 命中该位时把**首个**造成逃逸的写入点行号写进 EscLeakLine。-1 = 不采集(登记/落盘路径恒为 -1)。 + EscLeakWant: int + EscLeakLine: int def InitStructGen(self) -> int: """Initialize struct-mixin fields; called inline from subclass constructors.""" @@ -196,7 +201,9 @@ novirtual class StructGenMixin: self.EscConstEnv = None self.EscIndCount = 0 self.EscFieldTable = None - self.EscFieldCount = 0 + self.EscFieldCount = 0 + self.EscLeakWant = -1 + self.EscLeakLine = 0 self.StructNameIdx = None self.StructTypeIdx = None self.StructIdxCap = 0 @@ -600,9 +607,33 @@ novirtual class StructGenMixin: if ef_m != 0 or ne_m != 0: box[0] = (box[0] | sizet(ef_m)) & (~sizet(ne_m)) # 强制逃逸同样算**硬**位:调用点不能再按"同命"把它裁掉 - box[2] = (box[2] | sizet(ef_m)) & (~sizet(ne_m)) - return 0 - + box[2] = (box[2] | sizet(ef_m)) & (~sizet(ne_m)) + return 0 + + # escape_leak_line - **错误路径专用**:重扫本文件被调者函数体,取 want_pi 号形参的 + # "首个逃逸写入点"行号(0 = 未知)。只在 fail-fast 报错时调用一次,不进热路径; + # 跨模块拿不到 AST(EscFuncTable 按文件重置)时返回 0,调用点就不打泄漏点帧。 + def escape_leak_line(self, key: str, want_pi: int) -> int: + """返回被调者第 want_pi 号形参的首个逃逸点行号;0=未知(含跨模块)""" + if key is None or want_pi < 0: return 0 + fd: ast.FunctionDef* = self._EscFuncLookup(key) + if fd is None: return 0 + if self._EnsureRefineScratch() == 0: return 0 + g: sizet* = self.EscRefineScratch + if g is None: return 0 + box: sizet* = sizet*(uint64(g) + EC_BOX_SLOT * 8) + # 与 compute_escape_summary 同序(先收 global 名单再扫),但**不需要**常量特化/标注叠加: + # 这里只求"首个写入点行号",保守扫描已足够。 + g[0] = 0 + self._CollectGlobalNames(fd.children, g, 0) + self.EscConstEnv = None + self.EscIndCount = 0 + self.EscLeakWant = want_pi + self.EscLeakLine = 0 + self._ScanEscapes(fd, fd.children, g, box, 0) + self.EscLeakWant = -1 + return self.EscLeakLine + # ============================================================ # 跨过程常量特化(P3 收尾):调用点用常量实参重算被调者的逃逸摘要 # ============================================================ @@ -1333,6 +1364,14 @@ novirtual class StructGenMixin: if pa.arg is not None and string.strcmp(pa.arg, vnm.id) == 0: return int(i) return -1 + # _NoteLeak - 逃逸点采集(**只走错误路径**):escape_leak_line 重扫前把想要的形参位放进 + # EscLeakWant,这里在"置该位逃逸"的三个来源(字段/全局写、return 形参、传递闭包调用) + # 各调一次,记下**首个**造成逃逸的源码行号。 + def _NoteLeak(self, pi: int, node: ast.AST*) -> int: + if self.EscLeakWant < 0 or pi != self.EscLeakWant or self.EscLeakLine != 0: return 0 + if node is not None: self.EscLeakLine = node.lineno + return 0 + def _MarkEscapingStore(self, fd: ast.FunctionDef*, target: ast.AST*, value: ast.AST*, gset: sizet*, box: sizet*) -> int: """`target = 形参`:若 target 是对象字段或已声明的全局,则该形参逃逸""" pi: int = self._ParamIndex(fd, value) @@ -1353,6 +1392,8 @@ novirtual class StructGenMixin: if tnm.id is not None and gset is not None and self._IsGlobalNm(gset, tnm.id) != 0: box[0] = box[0] | (sizet(1) << pi) box[2] = box[2] | (sizet(1) << pi) + # 逃逸点采集(只在本类 escape_leak_line 指定的形参位上生效;登记/落盘路径 EscLeakWant=-1) + self._NoteLeak(pi, target) return 0 def _IsGlobalNm(self, garr: sizet*, name: str) -> int: @@ -1394,6 +1435,7 @@ novirtual class StructGenMixin: box[1] = 1 # 返回形参 = 硬逃逸(调用点无法再判定去向) box[2] = box[2] | (sizet(1) << pi) + self._NoteLeak(pi, st) elif k == ast.ASTKind.Assign: asg: ast.Assign* = ast.Assign*(st) if asg.targets is not None: @@ -1466,6 +1508,8 @@ novirtual class StructGenMixin: box[0] = box[0] | (sizet(1) << pi) # 传递闭包推上来的位同样算**硬**逃逸(被调者可能把它写进全局,这里看不到具体去向) box[2] = box[2] | (sizet(1) << pi) + # 泄漏点 = 本函数里这次"往下传"的调用行 + self._NoteLeak(pi, cl) return 0 # ---- M2.3 覆盖面扩展:类方法与嵌套函数的摘要 ---- diff --git a/App/lib/core/Gen/Type/TypeGenErr.vp b/App/lib/core/Gen/Type/TypeGenErr.vp index c5b768e..c874bca 100644 --- a/App/lib/core/Gen/Type/TypeGenErr.vp +++ b/App/lib/core/Gen/Type/TypeGenErr.vp @@ -39,8 +39,11 @@ novirtual class TypeGenErr(TypeGenBase.TypeGenBase): # 本方法是"必定终止"的 fail-fast,从不返回;但留空/None 都会让跨模块 call site 解析出 # 退化类型 i0,命中 CallGenCall.vp 的 i0 守卫 → _fail_method_ret_ty("fatal_error") 中止编译。 # 给 int 后解析为 i32,守卫不再命中;因从不返回,该类型不会被真正使用。 - def fatal_error(self, node: ast.AST*, msg: str, err_type: str = "SyntaxError", col_start: int = 0, col_end: int = 0) -> int: - """Report an error in Python style and exit (single diagnostic carrying its full call chain).""" + def fatal_error(self, node: ast.AST*, msg: str, err_type: str = "SyntaxError", col_start: int = 0, col_end: int = 0, leak_file: str = None, leak_line: int = 0) -> int: + """Report an error in Python style and exit (single diagnostic carrying its full call chain). + + leak_line > 0 时额外打印一帧 `File lifetime leak "", line N`(逃逸类错误的泄漏点)。 + """ # 当前帧(错误点)位置:优先 node,其次调用栈最内层有行号的帧,最后 current_file。 lineno: int = 0 if node is not None: lineno = node.lineno @@ -64,6 +67,7 @@ novirtual class TypeGenErr(TypeGenBase.TypeGenBase): # 极端情况(分配失败):退回单帧输出,保证错误不被静默吞掉。 Diag.emit_header() Diag.emit_current(file_name, lineno, col_start, col_end) + if leak_line > 0: Diag.emit_leak_frame(leak_file, leak_line) Diag.emit_category(err_type, msg) Diag.finish() return 0 @@ -75,6 +79,8 @@ novirtual class TypeGenErr(TypeGenBase.TypeGenBase): if frame is not None and frame.file_name is not None: Diag.add_frame(ent, frame.file_name, frame.lineno) Diag.set_current(ent, file_name, lineno, col_start, col_end) + # 泄漏点帧(可选):逃逸类错误指出"值是从被调者哪一行漏出去的" + Diag.set_leak(ent, leak_file, leak_line) # 默认"向上":最外层触发点在上、错误点最后(Python most recent call last)。 Diag.emit(ent, Diag.DIAG_DIR_UP) Diag.free_entry(ent)