feat(lifetime): 声明式指令契约落 .vpi(esce=/escn=),跨模块调用点也能最终裁决

写侧 Converter:读 compute_escape_summary 落在 box[3]/box[4] 的契约掩码,随函数头行尾写 esce=<强制逃逸位> / escn=<强制不逃逸位>;
只有指令(既无 esc/ret 也无 wf)时也补一个 "# esc=0 ret=0" 注释段——读侧是"见 # 才解析"。

读侧 VpiReader:_cache_vpi_esc_clears_text 同一次行扫描里顺带收 esce=/escn= 进 _g_vpi_esd_*(与 escg 同键 sha1[.Cls].func);
新增 _lookup_vpi_escape_dir(.., noescape);CallGenBase.lookup_escape_dir_via_vpi 包装。

调用点:模块级函数 / 方法 / 构造器三条路径在本文件指令表为空(= 跨模块)时改查对方 .vpi,
仍放在所有推断/回退之后最后套用:esc_mask = (esc_mask | esce) & ~escn。
This commit is contained in:
TermiNexus committed 2026-10-09 20:40:30 +08:00
1 parent 2bd37e80e3
commit 79299ec47d
5 files changed
+116 -7

No files matched your search

+19 -2
View File
@@ -522,11 +522,17 @@ def _PyiFunction(buf: bytes, buf_size: sizet, pos: sizet, fn: ast.FunctionDef*,
wf_mask_c: int = GenApi.compute_willfree_mask(fn)
esc_v_c: int = 0
ret_v_c: int = 0
ef_v_c: int = 0
ne_v_c: int = 0
if _PyiEnsureEscScratch() != 0:
esc_box_c: sizet* = sizet*(uint64(_g_pyi_esc_scratch) + 520)
if GenApi.compute_escape_summary(None, fn, _g_pyi_esc_scratch, esc_box_c) == 0:
esc_v_c = int(esc_box_c[0])
ret_v_c = int(esc_box_c[1])
# 声明式指令(@life(escape(...)) / @life(noescape(...))):compute_escape_summary 把
# 契约掩码落在 box[3]/box[4],随同行走 esce=/escn= 落盘,跨模块调用点才能最后套用。
ef_v_c = int(esc_box_c[3])
ne_v_c = int(esc_box_c[4])
# 跨模块常量特化:算"哪些常量绑定能清掉哪些逃逸位"(每条 = 打包的 p|k|m),随同行走 escg= 落盘。
# 只在真有逃逸位时才有意义;算出来 0 条就不写(保持 vpi 干净)。
clears_c: sizet* = None
@@ -535,13 +541,16 @@ def _PyiFunction(buf: bytes, buf_size: sizet, pos: sizet, fn: ast.FunctionDef*,
clears_c = sizet*(stdlib.malloc(4 * 8))
if clears_c is not None:
n_clears_c = GenApi.compute_escape_clears(fn, clears_c, 4)
if esc_v_c != 0 or ret_v_c != 0 or wf_mask_c != 0 or n_clears_c > 0:
if esc_v_c != 0 or ret_v_c != 0 or wf_mask_c != 0 or n_clears_c > 0 or ef_v_c != 0 or ne_v_c != 0:
tmp_c: bytes = stdlib.malloc(96)
if tmp_c is not None:
if esc_v_c != 0 or ret_v_c != 0:
if wf_mask_c != 0: viperlib.snprintf(tmp_c, 96, " # esc=%d ret=%d wf=%d", esc_v_c, ret_v_c, wf_mask_c)
else: viperlib.snprintf(tmp_c, 96, " # esc=%d ret=%d", esc_v_c, ret_v_c)
else: viperlib.snprintf(tmp_c, 96, " # wf=%d", wf_mask_c)
elif wf_mask_c != 0: viperlib.snprintf(tmp_c, 96, " # wf=%d", wf_mask_c)
# 只有声明式指令(既无逃逸/借用也无 willFree)时也要落一个 '#' 注释段:
# 读侧是"见到 '#' 才去找 esce=/escn=",没有 '#' 就整行都不看。
else: viperlib.snprintf(tmp_c, 96, " # esc=0 ret=0")
pos = _PyiEmit(buf, buf_size, pos, tmp_c)
# 跨模块常量特化:每条"常量守卫"落一个 escg= 字段(打包 p|k|m,见
# StructGenMixin._PackEscapeClear)。同一函数多条 → 重复该字段,读侧按同键全部收集。
@@ -551,6 +560,14 @@ def _PyiFunction(buf: bytes, buf_size: sizet, pos: sizet, fn: ast.FunctionDef*,
viperlib.snprintf(tmp_c, 96, " escg=%d", int(clears_c[ci_c]))
pos = _PyiEmit(buf, buf_size, pos, tmp_c)
ci_c += 1
# 声明式指令契约:escape(...) → esce=,noescape(...) → escn=(各自独立字段,
# 避免和 esc=/ret=/wf= 挤在同一个 96 字节 snprintf 里)。
if ef_v_c != 0:
viperlib.snprintf(tmp_c, 96, " esce=%d", ef_v_c)
pos = _PyiEmit(buf, buf_size, pos, tmp_c)
if ne_v_c != 0:
viperlib.snprintf(tmp_c, 96, " escn=%d", ne_v_c)
pos = _PyiEmit(buf, buf_size, pos, tmp_c)
stdlib.free(tmp_c)
if clears_c is not None: stdlib.free(clears_c)
pos = _PyiEmit(buf, buf_size, pos, "\n")
+71
View File
@@ -133,6 +133,12 @@ define VPI_ESG_ENV_PARAM_MAX = 64
_g_vpi_esg_keys: bytes = None
_g_vpi_esg_vals: sizet* = None
_g_vpi_esg_count: int = 0
# 声明式指令契约(`@life(escape(...))` / `@life(noescape(...))`):与 escg 表同键(sha1[.Cls].func),
# 但每函数只一条(契约唯一);两列分别存"强制逃逸位"与"强制不逃逸位"。
_g_vpi_esd_keys: bytes = None
_g_vpi_esd_e: sizet* = None
_g_vpi_esd_n: sizet* = None
_g_vpi_esd_count: int = 0
_g_vpi_esc_keys: bytes = None
_g_vpi_esc_vals: sizet* = None
@@ -895,6 +901,7 @@ def _ensure_vpi_ret_annot_loaded(src_sha1: str, temp_dir: str) -> int:
def _cache_vpi_esc_clears_text(vpi_buf: bytes, src_sha1: str) -> int:
"""抽取 vpi 行尾的 escg= 标记进缓存;1=处理完成 / 0=缓冲区或分配失败"""
global _g_vpi_esg_keys, _g_vpi_esg_vals, _g_vpi_esg_count
global _g_vpi_esd_keys, _g_vpi_esd_e, _g_vpi_esd_n, _g_vpi_esd_count
if vpi_buf is None or src_sha1 is None: return 0
if _g_vpi_esg_keys is None:
_g_vpi_esg_keys = stdlib.malloc(VPI_ESG_MAX * VPI_ESG_KEY_LEN)
@@ -902,6 +909,14 @@ def _cache_vpi_esc_clears_text(vpi_buf: bytes, src_sha1: str) -> int:
if _g_vpi_esg_keys is None or _g_vpi_esg_vals is None: return 0
string.memset(_g_vpi_esg_keys, 0, VPI_ESG_MAX * VPI_ESG_KEY_LEN)
string.memset(_g_vpi_esg_vals, 0, VPI_ESG_MAX * 8)
if _g_vpi_esd_keys is None:
_g_vpi_esd_keys = stdlib.malloc(VPI_ESG_MAX * VPI_ESG_KEY_LEN)
_g_vpi_esd_e = sizet*(stdlib.malloc(VPI_ESG_MAX * 8))
_g_vpi_esd_n = sizet*(stdlib.malloc(VPI_ESG_MAX * 8))
if _g_vpi_esd_keys is None or _g_vpi_esd_e is None or _g_vpi_esd_n is None: return 0
string.memset(_g_vpi_esd_keys, 0, VPI_ESG_MAX * VPI_ESG_KEY_LEN)
string.memset(_g_vpi_esd_e, 0, VPI_ESG_MAX * 8)
string.memset(_g_vpi_esd_n, 0, VPI_ESG_MAX * 8)
sha1_len: sizet = string.strlen(src_sha1)
buf_len: sizet = string.strlen(vpi_buf)
cur_cls: bytes = None
@@ -967,6 +982,34 @@ def _cache_vpi_esc_clears_text(vpi_buf: bytes, src_sha1: str) -> int:
j3 = kq
else:
j3 += 1
# 声明式指令契约(esce= / escn=):每函数一条,只认 '#' 注释段
de_v: int = 0
dn_v: int = 0
j4: sizet = hm + 1
while j4 + 5 <= le:
if vpi_buf[j4] == 'e' and vpi_buf[j4 + 1] == 's' and vpi_buf[j4 + 2] == 'c' and vpi_buf[j4 + 3] == 'e' and vpi_buf[j4 + 4] == '=':
kq2: sizet = j4 + 5
v2: int = 0
while kq2 < le and vpi_buf[kq2] >= '0' and vpi_buf[kq2] <= '9':
v2 = v2 * 10 + (int(vpi_buf[kq2]) - 48)
kq2 += 1
if v2 > 0: de_v = v2
j4 = kq2
elif vpi_buf[j4] == 'e' and vpi_buf[j4 + 1] == 's' and vpi_buf[j4 + 2] == 'c' and vpi_buf[j4 + 3] == 'n' and vpi_buf[j4 + 4] == '=':
kq3: sizet = j4 + 5
v3: int = 0
while kq3 < le and vpi_buf[kq3] >= '0' and vpi_buf[kq3] <= '9':
v3 = v3 * 10 + (int(vpi_buf[kq3]) - 48)
kq3 += 1
if v3 > 0: dn_v = v3
j4 = kq3
else:
j4 += 1
if (de_v != 0 or dn_v != 0) and _g_vpi_esd_count < VPI_ESG_MAX:
string.strcpy(_g_vpi_esd_keys + sizet(_g_vpi_esd_count) * VPI_ESG_KEY_LEN, key_buf_e)
_g_vpi_esd_e[_g_vpi_esd_count] = sizet(de_v)
_g_vpi_esd_n[_g_vpi_esd_count] = sizet(dn_v)
_g_vpi_esd_count += 1
if mname_buf_e is not None: stdlib.free(mname_buf_e)
if key_buf_e is not None: stdlib.free(key_buf_e)
pos = le + 1
@@ -1007,6 +1050,34 @@ def _lookup_vpi_escape_clears(src_sha1: str, cls_name: str, method_name: str, en
return cleared
# _lookup_vpi_escape_dir - 跨模块声明式指令契约查询(`@life(escape(...))` / `@life(noescape(...))`)。
# noescape != 0 取"强制不逃逸位",否则取"强制逃逸位";0 = 无该标注(含该模块未加载过)。
def _lookup_vpi_escape_dir(src_sha1: str, cls_name: str, method_name: str, noescape: int) -> int:
if _g_vpi_esd_keys is None or _g_vpi_esd_count == 0: return 0
if src_sha1 is None or method_name is None: return 0
sha1_len: sizet = string.strlen(src_sha1)
cl_len: sizet = 0
if cls_name is not None: cl_len = string.strlen(cls_name)
me_len: sizet = string.strlen(method_name)
key_len: sizet = sha1_len + 1 + cl_len + 1 + me_len
kb: bytes = stdlib.malloc(key_len + 1)
if kb is None: return 0
if cls_name is not None: viperlib.snprintf(kb, key_len + 1, "%s.%s.%s", src_sha1, cls_name, method_name)
else: viperlib.snprintf(kb, key_len + 1, "%s.%s", src_sha1, method_name)
r: int = 0
i: int = 0
while i < _g_vpi_esd_count:
ent: bytes = _g_vpi_esd_keys + sizet(i) * VPI_ESG_KEY_LEN
# 键以模块 sha1 打头:先比前 2 个十六进制字符,避免逐项 strcmp
if ent[0] == kb[0] and ent[1] == kb[1] and string.strcmp(ent, kb) == 0:
if noescape != 0: r = int(_g_vpi_esd_n[i])
else: r = int(_g_vpi_esd_e[i])
break
i += 1
stdlib.free(kb)
return r
# _cache_vpi_annots_text - 所有"从 vpi 文本解析"的缓存**单一入口**。
# 任何新增的文本缓存都加在这里,不要再散落到各加载点——漏喂一处就会**静默退化**
# (现场:逃逸摘要只在 _ensure_vpi_ret_annot_loaded 里喂,而 stub 注入加载点没喂,
+8
View File
@@ -806,6 +806,14 @@ novirtual class CallGenBase:
VpiReader._vpi_escape_mark_module(sha1)
return VpiReader._lookup_vpi_escape_packed(sha1, cls_name, method_name)
# lookup_escape_dir_via_vpi - 跨模块声明式指令契约(@life(escape(...)) / @life(noescape(...)))。
# 本文件指令表按文件重置,跨模块调用点必为空,只能靠 .vpi 行尾的 esce=/escn= 传递;
# noescape != 0 取"强制不逃逸位",否则取"强制逃逸位"。0 = 无该标注(含该模块未加载过)。
def lookup_escape_dir_via_vpi(self, cls_sha1: str, class_name: str, func_name: str, noescape: int) -> int:
"""对方模块 .vpi 里落的契约掩码;0 = 无该标注"""
if func_name is None or cls_sha1 is None: return 0
return VpiReader._lookup_vpi_escape_dir(cls_sha1, class_name, func_name, noescape)
def lookup_escape_via_vpi(self, pool: memhub.MemBuddy*, cls_sha1: str, cls_name: str, method_name: str) -> int:
"""跨模块逃逸摘要(v3):本类 + MRO 祖先各自的模块 sha1 依次查 .vpi,返回打包值(0=无摘要)。
+12 -2
View File
@@ -152,14 +152,19 @@ novirtual class CallGenCall(CallGenBase.CallGenBase):
# `__new__` 不查:其形参与 __init__ 共享且可能被裁剪,位置对应不可靠。
if arg_nodes is not None and string.strcmp(method_name, "__init__") == 0:
esc_mask_ci: int = GenApi.get_param_escapes_method(pool, class_name, method_name)
sha_ci: str = None
if esc_mask_ci == 0:
# 跨模块回退(v3):构造器常定义在别的模块(self.f = p 写在那边)
sha_ci: str = self.Gen.StructGen.get_struct_sha1(class_name)
sha_ci = self.Gen.StructGen.get_struct_sha1(class_name)
pk_ci: int = self.Gen.CallGenBase.lookup_escape_via_vpi(pool, sha_ci, class_name, method_name)
if pk_ci != 0: esc_mask_ci = int(pk_ci & 4294967295)
# 声明式指令最终裁决(@life(escape/noescape):unsafe 但强制),在推断/回退之后最后套用
f_ci: int = GenApi.get_param_escape_force_method(pool, class_name, method_name)
n_ci: int = GenApi.get_param_noescape_force_method(pool, class_name, method_name)
if f_ci == 0 and n_ci == 0 and sha_ci is not None:
# 跨模块:本文件指令表为空,改查对方模块 .vpi 的 esce=/escn=
f_ci = self.Gen.CallGenBase.lookup_escape_dir_via_vpi(sha_ci, class_name, method_name, 0)
n_ci = self.Gen.CallGenBase.lookup_escape_dir_via_vpi(sha_ci, class_name, method_name, 1)
if f_ci != 0 or n_ci != 0: esc_mask_ci = (esc_mask_ci | f_ci) & (~n_ci)
if esc_mask_ci != 0:
dummy_ptr_ci: mlirlite.LLVMType* = mlirlite.Ptr(pool, mlirlite.Int8(pool))
@@ -556,9 +561,10 @@ novirtual class CallGenCall(CallGenBase.CallGenBase):
cargs_mc: list[ast.AST*]* = cl.args
if cargs_mc is not None:
esc_mask_mc: int = GenApi.get_param_escapes_method(pool, class_name, method_name)
sha_mc: str = None
if esc_mask_mc == 0:
# 跨模块回退(v3):本文件摘要表按文件重置,跨模块只能查 .vpi 落盘的摘要
sha_mc: str = self.Gen.StructGen.get_struct_sha1(class_name)
sha_mc = self.Gen.StructGen.get_struct_sha1(class_name)
pk_mc: int = self.Gen.CallGenBase.lookup_escape_via_vpi(pool, sha_mc, class_name, method_name)
if pk_mc != 0: esc_mask_mc = int(pk_mc & 4294967295)
# 跨模块常量特化:用实参常量套用对方模块 .vpi 落盘的守卫记录(形参位含 self 偏移)
@@ -572,6 +578,10 @@ novirtual class CallGenCall(CallGenBase.CallGenBase):
# 声明式指令最终裁决(@life(escape/noescape):unsafe 但强制),在推断/回退之后最后套用
f_mc: int = GenApi.get_param_escape_force_method(pool, class_name, method_name)
n_mc: int = GenApi.get_param_noescape_force_method(pool, class_name, method_name)
if f_mc == 0 and n_mc == 0 and sha_mc is not None:
# 跨模块:本文件指令表为空,改查对方模块 .vpi 的 esce=/escn=
f_mc = self.Gen.CallGenBase.lookup_escape_dir_via_vpi(sha_mc, class_name, method_name, 0)
n_mc = self.Gen.CallGenBase.lookup_escape_dir_via_vpi(sha_mc, class_name, method_name, 1)
if f_mc != 0 or n_mc != 0: esc_mask_mc = (esc_mask_mc | f_mc) & (~n_mc)
if esc_mask_mc != 0:
dummy_ptr_mc: mlirlite.LLVMType* = mlirlite.Ptr(pool, mlirlite.Int8(pool))
+6 -3
View File
@@ -152,11 +152,10 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase):
# loc_mask:1=掩码来自**本文件**摘要表(只有这时才有"硬逃逸位"记录可用);
# 走下面的跨模块回退时置 0 —— 跨模块的掩码是合并值,不能再按"字段位"裁剪(会变成漏报)。
loc_mask: int = 1
# 跨模块回退解析出的被调者模块 sha1(最终裁决要用它去查对方 .vpi 的指令契约)
mod_sha_ec: str = None
if esc_mask == 0:
loc_mask = 0
# 跨模块回退(v3):本文件摘要表按文件重置,跨模块的被调者只能靠 .vpi 传递。
# 解析被调者所属模块 sha1:带模块前缀(mod.f)用限定名,否则用 from-import 别名。
mod_sha_ec: str = None
if cl.func is not None and cl.func.kind() == ast.ASTKind.Attribute:
at_ec: ast.Attribute* = ast.Attribute*(cl.func)
mod_p_ec: str = None
@@ -197,6 +196,10 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase):
# 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 and ne_m == 0 and mod_sha_ec is not None:
# 跨模块:本文件指令表为空(按文件重置),改查对方模块 .vpi 的行尾 esce=/escn=
fe_m = self.Gen.CallGenBase.lookup_escape_dir_via_vpi(mod_sha_ec, None, func_name, 0)
ne_m = self.Gen.CallGenBase.lookup_escape_dir_via_vpi(mod_sha_ec, None, func_name, 1)
if fe_m != 0 or ne_m != 0:
esc_mask = (esc_mask | fe_m) & (~ne_m)
if esc_mask != 0: