feat(diag): 逃逸类 LifetimeError 追加 File lifetime leak 帧,标出泄漏点
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,不打该帧。
This commit is contained in:
1 parent
c98730f717
commit
2353dca8ec
6 files changed
+112
-13
No files matched your search
@@ -54,6 +54,10 @@ class DiagEntry:
|
||||
FrameCount: int # 调用链帧数
|
||||
FrameFiles: sizet* # 调用链每帧文件名(深拷贝)
|
||||
FrameLines: sizet* # 调用链每帧行号(与 FrameFiles 同容量)
|
||||
# 泄漏点(可选):被调者那边**造成逃逸的写入点**。只有逃逸类 LifetimeError 会有值;
|
||||
# 打印成独立一帧 `File lifetime leak "<file>", 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.
|
||||
|
||||
@@ -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。
|
||||
|
||||
@@ -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 会被误判为"使用已失效变量"。
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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 覆盖面扩展:类方法与嵌套函数的摘要 ----
|
||||
|
||||
@@ -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 "<file>", 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)
|
||||
|
||||
Reference in new issue
Block a user