feat(lifetime): @life(escape/noescape) 升为调用点最终裁决(契约不可被推断推翻)

StructGenMixin: 新增 EscDirTable(3 槽/项:[名哈希, 强制逃逸位, 强制不逃逸位]),
_RegisterFuncEscapeSummary 把 compute_escape_summary 落在 box[3]/box[4] 的指令掩码单独落表
(与 EscTable 的"推断结果"分开);新增 _DirLookup / get_param_escape_force / get_param_noescape_force。
compute_escape_summary 的指令输出缓冲由 g+68 改为 box[3](同一内存,语义显式化)。

CallGenMixin: 在常量特化、同命裁剪、跨模块回退**全部做完之后**,最后套用一次指令:
esc_mask = (esc_mask | escape_force) & ~noescape_force。这样 escape(...) 不会被"死分支/同命"折掉,
noescape(...) 也不会被任何上游路径重新解释成"逃逸"。
This commit is contained in:
TermiNexus committed 2026-10-09 20:30:03 +08:00
1 parent 8881dea2e2
commit fc7c5d5560
3 files changed
+78 -9

No files matched your search

+7
View File
@@ -192,6 +192,13 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase):
if fld_only != 0:
cl_sf: int = self._FrameArgsEscapeClears(trans, cargs, can, fld_only)
if cl_sf != 0: esc_mask = esc_mask & (~cl_sf)
# ==== 声明式指令最终裁决(@life(escape/noescape):unsafe 但强制)====
# 必须放在所有推断/裁剪**之后**:常量特化、同命裁剪、跨模块回退都改不动契约。
# escape(...) 无条件置位、noescape(...) 无条件清位。
fe_m: int = GenApi.get_param_escape_force(func_name)
ne_m: int = GenApi.get_param_noescape_force(func_name)
if fe_m != 0 or ne_m != 0:
esc_mask = (esc_mask | fe_m) & (~ne_m)
if esc_mask != 0:
_dummy_ptr: mlirlite.LLVMType* = mlirlite.Ptr(pool, mlirlite.Int8(pool))
for ai in range(can):
+14
View File
@@ -497,6 +497,20 @@ def get_param_escapes_hard_method(pool: memhub.MemBuddy*, class_name: str, metho
return 0
# ---- 声明式指令(@life(escape/noescape))的最终裁决掩码 ----
# 契约表与推断表分开:调用点在所有推断/裁剪之后最后套用,任何推导都不能推翻。
def get_param_escape_force(name: str) -> int:
"""`@life(escape(...))` 强制逃逸位掩码;0 = 无该标注"""
if _g_gen is not None: return _g_gen.StructGen.get_param_escape_force(name)
return 0
def get_param_noescape_force(name: str) -> int:
"""`@life(noescape(...))` 强制不逃逸位掩码;0 = 无该标注"""
if _g_gen is not None: return _g_gen.StructGen.get_param_noescape_force(name)
return 0
# escape_leak_line - **错误路径专用**:被调者第 want_pi 号形参的"首个逃逸写入点"行号。
# 只在 fail-fast 报错时调用(会重扫一次被调者函数体);跨模块拿不到 AST 时返回 0。
def escape_leak_line(name: str, want_pi: int) -> int:
+57 -9
View File
@@ -173,7 +173,12 @@ novirtual class StructGenMixin:
# escape_leak_line 重扫被调者函数体前,把"想要的形参位"放进 EscLeakWant;_MarkEscapingStore
# 命中该位时把**首个**造成逃逸的写入点行号写进 EscLeakLine。-1 = 不采集(登记/落盘路径恒为 -1)。
EscLeakWant: int
EscLeakLine: int
EscLeakLine: int
# ── 声明式指令表(`@life(escape(...))` / `@life(noescape(...))`)──
# 3 槽/项:[名哈希, 强制逃逸位, 强制不逃逸位]。调用点在**所有推断与裁剪之后**最后套用,
# 是不可被推翻的契约("unsafe 但强制")。每文件重置计数。
EscDirTable: sizet*
EscDirCount: int
def InitStructGen(self) -> int:
"""Initialize struct-mixin fields; called inline from subclass constructors."""
@@ -203,7 +208,9 @@ novirtual class StructGenMixin:
self.EscFieldTable = None
self.EscFieldCount = 0
self.EscLeakWant = -1
self.EscLeakLine = 0
self.EscLeakLine = 0
self.EscDirTable = None
self.EscDirCount = 0
self.StructNameIdx = None
self.StructTypeIdx = None
self.StructIdxCap = 0
@@ -358,6 +365,11 @@ novirtual class StructGenMixin:
self.EscFieldTable = sizet*(esc_pool.alloc(esc_cap * 16))
if self.EscFieldTable is not None: string.memset(self.EscFieldTable, 0, esc_cap * 16)
self.EscFieldCount = 0
# 声明式指令表(escape/noescape):同样只分配一次,每文件重置计数
if self.EscDirTable is None:
self.EscDirTable = sizet*(esc_pool.alloc(esc_cap * 24))
if self.EscDirTable is not None: string.memset(self.EscDirTable, 0, esc_cap * 24)
self.EscDirCount = 0
self.EscFuncCount = 0
self.EscConstEnv = None
self.EscIndCount = 0
@@ -512,11 +524,36 @@ novirtual class StructGenMixin:
if idx < 0: return 0
return int(self.EscTable[idx * 3 + 1])
def get_ret_borrow(self, name: str) -> int:
"""返回借用标记:1=该函数直接返回某形参;无记录返回 0"""
idx: int = self._EscLookup(name)
if idx < 0: return 0
return int(self.EscTable[idx * 3 + 2])
def get_ret_borrow(self, name: str) -> int:
"""返回借用标记:1=该函数直接返回某形参;0=无记录"""
idx: int = self._EscLookup(name)
if idx < 0: return 0
return int(self.EscTable[idx * 3 + 2])
# ---- 声明式指令(@life(escape/noescape)):调用点的**最终裁决**表 ----
# 与 EscTable 分开存:EscTable 的掩码是"推断结果"(能被常量特化/同命裁剪改动),
# 指令是"契约"(不能被任何推断推翻),调用点在所有推断之后最后套用一次。
def _DirLookup(self, name: str) -> int:
"""指令表项下标;未找到返回 -1"""
if name is None or self.EscDirTable is None: return -1
h: sizet = self._NameHash(name)
i: int = 0
while i < self.EscDirCount:
if self.EscDirTable[i * 3] == h: return i
i += 1
return -1
def get_param_escape_force(self, name: str) -> int:
"""`@life(escape(...))` 强制逃逸位掩码;0 = 无该标注"""
idx: int = self._DirLookup(name)
if idx < 0: return 0
return int(self.EscDirTable[idx * 3 + 1])
def get_param_noescape_force(self, name: str) -> int:
"""`@life(noescape(...))` 强制不逃逸位掩码;0 = 无该标注"""
idx: int = self._DirLookup(name)
if idx < 0: return 0
return int(self.EscDirTable[idx * 3 + 2])
# ---- P2 willFree:声明式标注表(直接来自 @life(willFree(...)) / @willFree(...),不做数据流) ----
def _WillFreeLookup(self, name: str) -> int:
@@ -599,8 +636,10 @@ novirtual class StructGenMixin:
# 显式标注(声明式契约)**在扫描之后**叠加:escape 无条件置位、noescape 无条件清位。
# 顺序是关键:常量折叠发生在扫描里,所以 `escape(p)` 不会被"死分支"折掉 —— 契约的本意
# 正是"编译器看不见的路径";`noescape(p)` 也不会被任何推导重新置位。
# 借 g 的空槽(68/69,此刻 env 已用完)当输出缓冲,不依赖任何额外 scratch。
ann_out: sizet* = sizet*(uint64(g) + 68 * 8)
# 输出缓冲借用 box[3]/box[4](= 槽 68/69,此刻 env 已用完),不依赖额外 scratch;
# 这两个槽同时把**声明式指令**交给调用方(_RegisterFuncEscapeSummary 落 EscDirTable →
# 调用点最后再套用一次,那是不可被推断推翻的最终裁决)。
ann_out: sizet* = sizet*(uint64(box) + 3 * 8)
if GenApi.compute_escape_directives(fd, ann_out) == 0:
ef_m: int = int(ann_out[0])
ne_m: int = int(ann_out[1])
@@ -1311,6 +1350,15 @@ novirtual class StructGenMixin:
self.EscFuncTable[fs * 2 + 0] = sizet(self._NameHash(key))
self.EscFuncTable[fs * 2 + 1] = sizet(fd)
self.EscFuncCount = fs + 1
# 声明式指令(@life(escape/noescape))单独落表:调用点在所有推断/裁剪之后最后套用。
# box[3]/box[4] 由 compute_escape_summary 借给 compute_escape_directives 当输出(无标注时恒 0)。
if self.EscDirTable is not None and self.EscDirCount < 1024:
if box[3] != 0 or box[4] != 0:
d_s: int = self.EscDirCount
self.EscDirTable[d_s * 3 + 0] = sizet(self._NameHash(key))
self.EscDirTable[d_s * 3 + 1] = box[3]
self.EscDirTable[d_s * 3 + 2] = box[4]
self.EscDirCount = d_s + 1
return 0
def _CollectGlobalNames(self, stmts: list[ast.AST*]*, gset: sizet*, depth: int) -> int: