feat(gen): 落地 @life(ret(...)) 返回值生命期契约(A2)

- GenApi:新增 compute_life_anno / _RetKindFromTag 与 RET_KIND_* 编码,
  提供 get_ret_kind / get_ret_kind_method / lookup_ret_kind_via_vpi;
  _WillFreeMergeTag 不再拦截 ret(改由 compute_life_anno 收集校验)。
- StructGenMixin:EscDirTable 由 3 槽扩到 4 槽(新增 ret_kind 列),
  compute_escape_summary 把契约落在 box[5] 并登记。
- Stub:.vpi 行尾新增 escr=<ret_kind>(Converter 写、VpiReader 读 _g_vpi_esd_r)。
- AssignGenMixin.life_of_expr(Call):@life(ret(...)) 声明优先于任何推断;
  frame/heap/static/borrows=none 直接映射,borrows=x 取对应实参生命期,
  方法调用按接收者静态类型解析类名。
- StmtGenCifMixin:同批移除定义端 return <本帧地址> 的 fail-fast,
  返回值去向改由调用点按被调者契约判定。
This commit is contained in:
TermiNexus committed 2026-10-09 22:58:33 +08:00
1 parent 4907e64714
commit 6da8e2e095
7 files changed
+347 -56

No files matched your search

+8 -1
View File
@@ -524,6 +524,7 @@ def _PyiFunction(buf: bytes, buf_size: sizet, pos: sizet, fn: ast.FunctionDef*,
ret_v_c: int = 0
ef_v_c: int = 0
ne_v_c: int = 0
rk_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:
@@ -533,6 +534,8 @@ def _PyiFunction(buf: bytes, buf_size: sizet, pos: sizet, fn: ast.FunctionDef*,
# 契约掩码落在 box[3]/box[4],随同行走 esce=/escn= 落盘,跨模块调用点才能最后套用。
ef_v_c = int(esc_box_c[3])
ne_v_c = int(esc_box_c[4])
# A2(§14)`@life(ret(...))`:返回值生命期契约落 box[5],随同行走 escr= 落盘。
rk_v_c = int(esc_box_c[5])
# 跨模块常量特化:算"哪些常量绑定能清掉哪些逃逸位"(每条 = 打包的 p|k|m),随同行走 escg= 落盘。
# 只在真有逃逸位时才有意义;算出来 0 条就不写(保持 vpi 干净)。
clears_c: sizet* = None
@@ -541,7 +544,7 @@ 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 or ef_v_c != 0 or ne_v_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 or rk_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:
@@ -568,6 +571,10 @@ def _PyiFunction(buf: bytes, buf_size: sizet, pos: sizet, fn: ast.FunctionDef*,
if ne_v_c != 0:
viperlib.snprintf(tmp_c, 96, " escn=%d", ne_v_c)
pos = _PyiEmit(buf, buf_size, pos, tmp_c)
# A2(§14):`@life(ret(...))` 的返回值生命期契约 → escr=(编码见 GenApi.RET_KIND_*)。
if rk_v_c != 0:
viperlib.snprintf(tmp_c, 96, " escr=%d", rk_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")
+26 -10
View File
@@ -133,11 +133,13 @@ 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),
# 但每函数只一条(契约唯一);两列分别存"强制逃逸位"与"强制不逃逸位"。
# 声明式指令契约(`@life(escape(...))` / `@life(noescape(...))` / `@life(ret(...))`):与 escg 表同键
# (sha1[.Cls].func),但每函数只一条(契约唯一);三列分别存"强制逃逸位"、"强制不逃逸位"与
# ret_kind(A2,编码见 GenApi.RET_KIND_*)。
_g_vpi_esd_keys: bytes = None
_g_vpi_esd_e: sizet* = None
_g_vpi_esd_n: sizet* = None
_g_vpi_esd_r: sizet* = None
_g_vpi_esd_count: int = 0
_g_vpi_esc_keys: bytes = None
@@ -901,7 +903,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
global _g_vpi_esd_keys, _g_vpi_esd_e, _g_vpi_esd_n, _g_vpi_esd_r, _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)
@@ -913,10 +915,12 @@ def _cache_vpi_esc_clears_text(vpi_buf: bytes, src_sha1: str) -> int:
_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
_g_vpi_esd_r = 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 or _g_vpi_esd_r 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)
string.memset(_g_vpi_esd_r, 0, VPI_ESG_MAX * 8)
sha1_len: sizet = string.strlen(src_sha1)
buf_len: sizet = string.strlen(vpi_buf)
cur_cls: bytes = None
@@ -982,9 +986,10 @@ def _cache_vpi_esc_clears_text(vpi_buf: bytes, src_sha1: str) -> int:
j3 = kq
else:
j3 += 1
# 声明式指令契约(esce= / escn=):每函数一条,只认 '#' 注释段
# 声明式指令契约(esce= / escn= / escr=):每函数一条,只认 '#' 注释段
de_v: int = 0
dn_v: int = 0
dr_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] == '=':
@@ -1003,12 +1008,21 @@ def _cache_vpi_esc_clears_text(vpi_buf: bytes, src_sha1: str) -> int:
kq3 += 1
if v3 > 0: dn_v = v3
j4 = kq3
elif vpi_buf[j4] == 'e' and vpi_buf[j4 + 1] == 's' and vpi_buf[j4 + 2] == 'c' and vpi_buf[j4 + 3] == 'r' and vpi_buf[j4 + 4] == '=':
kq5: sizet = j4 + 5
v5: int = 0
while kq5 < le and vpi_buf[kq5] >= '0' and vpi_buf[kq5] <= '9':
v5 = v5 * 10 + (int(vpi_buf[kq5]) - 48)
kq5 += 1
if v5 > 0: dr_v = v5
j4 = kq5
else:
j4 += 1
if (de_v != 0 or dn_v != 0) and _g_vpi_esd_count < VPI_ESG_MAX:
if (de_v != 0 or dn_v != 0 or dr_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_r[_g_vpi_esd_count] = sizet(dr_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)
@@ -1050,9 +1064,10 @@ 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:
# _lookup_vpi_escape_dir - 跨模块声明式指令契约查询(`@life(escape(...))` / `@life(noescape(...))` /
# `@life(ret(...))`)。which: 0=强制逃逸位 / 1=强制不逃逸位 / 2=ret_kind(A2)。
# 0 = 无该标注(含该模块未加载过)。
def _lookup_vpi_escape_dir(src_sha1: str, cls_name: str, method_name: str, which: 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)
@@ -1070,7 +1085,8 @@ def _lookup_vpi_escape_dir(src_sha1: str, cls_name: str, method_name: str, noesc
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])
if which == 2: r = int(_g_vpi_esd_r[i])
elif which != 0: r = int(_g_vpi_esd_n[i])
else: r = int(_g_vpi_esd_e[i])
break
i += 1
+101 -4
View File
@@ -588,9 +588,94 @@ novirtual class AssignGenMixin:
# 标签常量见 GenTypes define LIFE_*;有序,越小越"短命",汇合取 min。
# ============================================================
# life_of_expr - 求表达式当前值的生命期标签。
# target_ty 参与判定:裸本帧数组名在**指针**目标位置 = 数组退让(交出本帧栈地址),
# 在同型聚合目标位置 = 按值拷贝(LIFE_VALUE)。
# ── A2(§14)`@life(ret(...))`:调用点解析被调者的返回值生命期契约 ──
# _RecvClassName - 从方法的接收者表达式取它静态类型指向的类名(去 sha1 前缀);判不定返回 None。
# 只认「名字变量」接收者:变量表里查不到(如 `m.f()` 里的模块名 `m`)返回 None —— 调用方据此
# 把"模块限定函数调用"与"方法调用"分开(二者在 AST 里都是 Attribute)。
def _RecvClassName(self, trans: HT.Translator*, recv: ast.AST*) -> str:
"""接收者静态类型 → 类名(无 sha1 前缀);判不定返回 None"""
if trans is None or recv is None: return None
if recv.kind() != ast.ASTKind.Name: return None
rn: ast.Name* = ast.Name*(recv)
if rn.id is None: return None
v: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, rn.id)
if v is None: return None
ty: mlirlite.LLVMType* = v.Ty # 变量槽类型 = <变量类型>*
if ty is None or self.Gen.ExprGenBase.is_ptr_type(ty) == 0: return None
p1: mlirlite.LLVMType* = ty.Pointee # 变量类型本身
if p1 is None: return None
# 类变量通常是指针(`S*`);值语义的类变量(`s: S`,罕见)直接取本类型。
elem: mlirlite.LLVMType* = p1.Pointee
if self.Gen.ExprGenBase.is_ptr_type(p1) == 0: elem = p1
sname: str = mlirlite.get_struct_name(elem)
if sname is None: return None
pl: int = GenApi.sha1_prefix_len(sname)
if pl > 0: sname = sname + pl
return sname
# _CallArgNodeForParam - `@life(ret(borrows=<形参 pi>))` 在调用点对应的实参节点。
# 方法调用的形参表含 self(pi=0 即接收者),故方法分支要偏移一格。
def _CallArgNodeForParam(self, cl: ast.Call*, pi: int) -> ast.AST*:
"""第 pi 号形参对应的实参表达式;越界/判不定返回 None"""
if cl is None or pi < 0: return None
cargs: list[ast.AST*]* = cl.args
if cl.func is not None and cl.func.kind() == ast.ASTKind.Attribute:
if pi == 0:
at: ast.Attribute* = ast.Attribute*(cl.func)
return at.value
if cargs is None or sizet(pi - 1) >= len(cargs): return None
return cargs.get(sizet(pi - 1))
if cargs is None or sizet(pi) >= len(cargs): return None
return cargs.get(sizet(pi))
# _CallRetKind - 解析被调者的 `@life(ret(...))` 契约(本文件表 → 跨模块 .vpi);-1 = 无声明/判不定。
def _CallRetKind(self, trans: HT.Translator*, cl: ast.Call*) -> int:
"""被调者 ret_kind 编码(GenApi.RET_KIND_*);-1 = 无声明/判不定"""
if cl is None or cl.func is None: return -1
fnm: str = self.Gen.ExprGenBase.get_func_name(cl.func)
if fnm is None: return -1
pool: memhub.MemBuddy* = None
if trans is not None: pool = trans.Pool
if pool is None: return -1
# 方法调用:接收者是本帧变量 → 取它静态类型指向的类名;接收者是模块名 → 不当方法。
if cl.func.kind() == ast.ASTKind.Attribute:
at: ast.Attribute* = ast.Attribute*(cl.func)
cls: str = self._RecvClassName(trans, at.value)
if cls is not None:
rk_m: int = GenApi.get_ret_kind_method(pool, cls, fnm)
if rk_m != 0: return rk_m
sha_m: str = self.Gen.StructGen.get_struct_sha1(cls)
if sha_m is not None:
rk_m = self.Gen.CallGenBase.lookup_ret_kind_via_vpi(sha_m, cls, fnm)
if rk_m != 0: return rk_m
return -1
# 模块级函数(裸名,或模块限定 `m.f()`):本文件表优先,再跨模块 .vpi 回退。
rk: int = GenApi.get_ret_kind(fnm)
if rk != 0: return rk
mod_sha: str = None
if cl.func.kind() == ast.ASTKind.Attribute:
at2: ast.Attribute* = ast.Attribute*(cl.func)
# 只认裸模块名接收者(`m.f()`);`self.h.f()` 之类链式接收者判不定 → 不猜。
if at2.value is None or at2.value.kind() != ast.ASTKind.Name: return -1
mn2: ast.Name* = ast.Name*(at2.value)
mod_p: str = None
if mn2.id is not None: mod_p = mn2.id
if mod_p is not None:
fi2: str = None
if trans is not None: fi2 = trans._from_imports
mod_sha = self.Gen.StructGen._resolve_module_sha1(mod_p, fi2)
# 接收者既不是模块(解析不出 sha1)也不是变量(否则上面早按方法走了)→ 判不定。
if mod_sha is None: return -1
if mod_sha is None and trans is not None and trans._from_imports is not None:
mod_sha = self.Gen.CallGenBase._lookup_mod_sha1(pool, trans._from_imports, fnm, 0)
if mod_sha is not None:
rk = self.Gen.CallGenBase.lookup_ret_kind_via_vpi(mod_sha, None, fnm)
if rk != 0: return rk
return -1
# life_of_expr - 求表达式当前值的生命期标签。
# target_ty 参与判定:裸本帧数组名在**指针**目标位置 = 数组退让(交出本帧栈地址),
# 在同型聚合目标位置 = 按值拷贝(LIFE_VALUE)。
def life_of_expr(self, trans: HT.Translator*, node: ast.AST*, target_ty: mlirlite.LLVMType*) -> int:
"""表达式的 LifeTag;无法判定时 LIFE_UNKNOWN(保守放行)"""
if node is None or trans is None: return GenTypes.LIFE_VALUE
@@ -627,8 +712,20 @@ novirtual class AssignGenMixin:
return GenTypes.LIFE_STATIC
return GenTypes.LIFE_UNKNOWN
if k == ast.ASTKind.Call:
# 分配类调用 → 堆;其余调用 → 未知(函数摘要落地前保守放行)
cl: ast.Call* = ast.Call*(node)
# §14 A2:`@life(ret(...))` 声明**优先于任何推断**(无条件)。
rk: int = self._CallRetKind(trans, cl)
if rk >= 0:
if rk == GenApi.RET_KIND_FRAME: return GenTypes.LIFE_FRAME
if rk == GenApi.RET_KIND_HEAP: return GenTypes.LIFE_HEAP
if rk == GenApi.RET_KIND_STATIC: return GenTypes.LIFE_STATIC
if rk == GenApi.RET_KIND_NONFRAME: return GenTypes.LIFE_UNKNOWN
if rk >= GenApi.RET_KIND_BORROW_BASE:
pi: int = rk - GenApi.RET_KIND_BORROW_BASE
bn: ast.AST* = self._CallArgNodeForParam(cl, pi)
if bn is None: return GenTypes.LIFE_UNKNOWN
return self.life_of_expr(trans, bn, None)
# 分配类调用 → 堆;其余调用 → 未知(函数摘要落地前保守放行)
if cl.func is not None and cl.func.kind() == ast.ASTKind.Attribute:
at: ast.Attribute* = ast.Attribute*(cl.func)
if at.attr is not None:
+13
View File
@@ -814,6 +814,19 @@ novirtual class CallGenBase:
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)
# lookup_ret_kind_via_vpi - 跨模块 `@life(ret(...))` 返回值生命期契约(A2,§14)。
# 与 escape/noescape 同一条 .vpi 通道(行尾 escr=);0 = 无声明(含该模块未加载过)。
# 注意:本查询可能在调用点**之前**(life_of_expr 判右值生命期)发生,不能假定调用点的
# 逃逸查询已把对方 vpi 载入,故这里自己走一次"本模块已加载"备忘 + 按需加载。
def lookup_ret_kind_via_vpi(self, cls_sha1: str, class_name: str, func_name: str) -> int:
"""对方模块 .vpi 里落的 ret_kind;0 = 无声明"""
if func_name is None or cls_sha1 is None: return 0
if VpiReader._vpi_escape_module_loaded(cls_sha1) == 0:
if self.Gen.CallGenBase.TempDir is None or self.Gen.CallGenBase.TempDirLen == 0: return 0
VpiReader._ensure_vpi_ret_annot_loaded(cls_sha1, self.Gen.CallGenBase.TempDir)
VpiReader._vpi_escape_mark_module(cls_sha1)
return VpiReader._lookup_vpi_escape_dir(cls_sha1, class_name, func_name, 2)
def lookup_escape_via_vpi(self, pool: memhub.MemBuddy*, cls_sha1: str, cls_name: str, method_name: str) -> int:
"""跨模块逃逸摘要(v3):本类 + MRO 祖先各自的模块 sha1 依次查 .vpi,返回打包值(0=无摘要)。
+138 -11
View File
@@ -371,9 +371,12 @@ def _WillFreeMergeTag(fn: ast.FunctionDef*, tag: ast.AST*, mask: int) -> int:
# 二者复用同一对契约位,位本身由 compute_escape_directives 收。
is_st: int = string.strcmp(tnm, "stores")
is_bw: int = string.strcmp(tnm, "borrows")
# A2/A3 的标签(ret / owns)先**显式**报"未实现",不做静默忽略(静默 = 用户以为标注生效了)。
if string.strcmp(tnm, "owns") == 0 or string.strcmp(tnm, "ret") == 0:
fatal_error(tag, f"@life tag `{tnm}(...)` is not implemented yet (planned A2/A3); use willFree/escape/noescape/stores/borrows for now", "LifetimeError")
# A3 的标签(owns)先**显式**报"未实现",不做静默忽略(静默 = 用户以为标注生效了)。
if string.strcmp(tnm, "owns") == 0:
fatal_error(tag, "@life tag `owns(...)` is not implemented yet (planned A3); use willFree/escape/noescape/stores/borrows for now", "LifetimeError")
return mask
# A2 的 `ret(...)` 由 compute_life_anno 收集与校验(返回值生命期契约,不进 willFree 掩码)。
if string.strcmp(tnm, "ret") == 0:
return mask
if is_wf != 0 and is_esc != 0 and is_nesc != 0 and is_st != 0 and is_bw != 0:
fatal_error(tag, "@life(...) accepts only `willFree(...)` / `escape(...)` / `noescape(...)` / `stores(...)` / `borrows(...)` tags", "LifetimeError")
@@ -456,19 +459,143 @@ def compute_escape_directives(fd: ast.AST*, out: sizet*) -> int:
return 0
# ---- A2(§14)`@life(ret(...))`:返回值生命期契约的编码 ----
# 该编码同时用于 StructGenMixin.EscDirTable 的第 4 槽与 .vpi 行尾 `escr=`。
define RET_KIND_NONE = 0 # 无声明(= 回落到现有推断)
define RET_KIND_FRAME = 1 # `ret(frame)`:返回值可能指向**本帧**存储
define RET_KIND_HEAP = 2 # `ret(heap)`:返回值是自由生命期(可自由传递)
define RET_KIND_STATIC = 3 # `ret(static)`:返回值指向静态存储
define RET_KIND_NONFRAME = 4 # `ret(borrows=none)`:不借用任何形参 → 非本帧
define RET_KIND_BORROW_BASE = 8 # `ret(borrows=x)`:值 = RET_KIND_BORROW_BASE + 形参下标
# compute_life_anno - 收 `@life(ret(...))` 的返回值生命期契约到 out[0](ret_kind 编码)。
# 与 compute_escape_directives 同构:只认 `@life(...)` 里的 `ret` 标签;矛盾/未知一律 fail-fast。
# 语法(§14.1):`ret(frame)` / `ret(heap)` / `ret(static)` / `ret(borrows=<形参名或序号>)` / `ret(borrows=none)`。
def compute_life_anno(fd: ast.AST*, out: sizet*) -> int:
"""解析 @life(ret(...)) 到 out[0](ret_kind 编码);0=完成 / 1=入参非法"""
if out is None: return 1
out[0] = 0
fn: ast.FunctionDef* = ast.FunctionDef*(fd)
if fn is None or fn.decorator_list is None: return 0
dl: list[ast.AST*]* = fn.decorator_list
dn: sizet = len(dl)
i: sizet = 0
while i < dn:
deco: ast.AST* = dl.get(i)
i += 1
if deco is None or deco.kind() != ast.ASTKind.Call: continue
dc: ast.Call* = ast.Call*(deco)
if dc.func is None or dc.func.kind() != ast.ASTKind.Name: continue
fname: ast.Name* = ast.Name*(dc.func)
if fname.id is None or string.strcmp(fname.id, "life") != 0: continue
if dc.args is None: continue
an: sizet = len(dc.args)
j: sizet = 0
while j < an:
tg: ast.AST* = dc.args.get(j)
j += 1
if tg is None or tg.kind() != ast.ASTKind.Call: continue
tcl: ast.Call* = ast.Call*(tg)
tnm: str = None
if tcl.func is not None and tcl.func.kind() == ast.ASTKind.Name:
tn: ast.Name* = ast.Name*(tcl.func)
tnm = tn.id
if tnm is None or string.strcmp(tnm, "ret") != 0: continue
rk: int = _RetKindFromTag(fn, tcl)
if rk < 0: continue # 已 fail-fast
if int(out[0]) != 0 and int(out[0]) != rk:
fatal_error(tg, "contradictory @life(ret(...)) declarations on the same function", "LifetimeError")
continue
out[0] = sizet(rk)
return 0
def _RetKindFromTag(fn: ast.FunctionDef*, tcl: ast.Call*) -> int:
"""把一个 `ret(...)` 标签解析成 ret_kind 编码;非法即 fail-fast 并返回 -1"""
# 形式一:ret(borrows=x) / ret(borrows=none)
if tcl.keywords is not None and len(tcl.keywords) > 0:
if len(tcl.keywords) != 1:
fatal_error(tcl, "@life(ret(...)) accepts exactly one keyword: `borrows=<param>` or `borrows=none`", "LifetimeError")
return -1
if tcl.args is not None and len(tcl.args) > 0:
fatal_error(tcl, "@life(ret(...)): cannot mix positional arguments with `borrows=`", "LifetimeError")
return -1
kw: ast.AST* = tcl.keywords.get(0)
if kw is None or kw.kind() != ast.ASTKind.Keyword:
fatal_error(tcl, "@life(ret(...)) accepts only `ret(frame|heap|static)` or `ret(borrows=<param>|none)`", "LifetimeError")
return -1
ka: ast.Keyword* = ast.Keyword*(kw)
if ka.arg is None or string.strcmp(ka.arg, "borrows") != 0:
fatal_error(tcl, "@life(ret(...)): the only supported keyword is `borrows`", "LifetimeError")
return -1
val: ast.AST* = ka.value
if val is None:
fatal_error(tcl, "@life(ret(borrows=...)) requires a parameter name or `none`", "LifetimeError")
return -1
if val.kind() == ast.ASTKind.Name:
vn: ast.Name* = ast.Name*(val)
if vn.id is None: return -1
if string.strcmp(vn.id, "none") == 0: return RET_KIND_NONFRAME
idx: int = _WillFreeArgIndex(fn, val)
if idx < 0: return -1 # 已 fail-fast
return RET_KIND_BORROW_BASE + idx
if val.kind() == ast.ASTKind.Constant:
vc: ast.Constant* = ast.Constant*(val)
if vc is not None and vc.const_kind == ast.CONST_NONE: return RET_KIND_NONFRAME
fatal_error(tcl, "@life(ret(borrows=...)) accepts only a parameter name or `none`", "LifetimeError")
return -1
# 形式二:ret(frame) / ret(heap) / ret(static)
if tcl.args is None or len(tcl.args) != 1:
fatal_error(tcl, "@life(ret(...)) expects exactly one of `frame` / `heap` / `static`, or `borrows=<param>` / `borrows=none`", "LifetimeError")
return -1
a0: ast.AST* = tcl.args.get(0)
if a0 is None or a0.kind() != ast.ASTKind.Name:
fatal_error(tcl, "@life(ret(...)) expects exactly one of `frame` / `heap` / `static`", "LifetimeError")
return -1
an: ast.Name* = ast.Name*(a0)
if an.id is None: return -1
if string.strcmp(an.id, "frame") == 0: return RET_KIND_FRAME
if string.strcmp(an.id, "heap") == 0: return RET_KIND_HEAP
if string.strcmp(an.id, "static") == 0: return RET_KIND_STATIC
fatal_error(a0, f"@life(ret({an.id})): expected `frame` / `heap` / `static`, or `borrows=<param>` / `borrows=none`", "LifetimeError")
return -1
def get_ret_kind(name: str) -> int:
"""本文件函数的 `@life(ret(...))` 契约编码;0 = 无声明"""
if _g_gen is not None: return _g_gen.StructGen.get_ret_kind(name)
return 0
def get_ret_kind_method(pool: memhub.MemBuddy*, class_name: str, method_name: str) -> int:
"""方法版 `@life(ret(...))` 契约:查 "Class.method" 并沿 MRO 回退;0 = 无声明"""
if _g_gen is not None: return _g_gen.StructGen.get_ret_kind_method(pool, class_name, method_name)
return 0
def lookup_ret_kind_via_vpi(cls_sha1: str, class_name: str, func_name: str) -> int:
"""跨模块给被调者的 `@life(ret(...))` 契约;0 = 无声明(含该模块未加载)"""
if _g_gen is not None: return _g_gen.CallGenBase.lookup_ret_kind_via_vpi(cls_sha1, class_name, func_name)
return 0
def _WillFreeArgIndex(fn: ast.FunctionDef*, arg: ast.AST*) -> int:
"""标签实参 → 形参下标(0 起,self/cls 计入);非法即报错并返回 -1"""
"""标签实参 → 形参下标(0 起,self/cls 计入);非法即报错并返回 -1。
被 `willFree` / `escape|stores` / `noescape|borrows` / `ret(borrows=...)` 共用,
故报文统一用"@life 标签"口径。"""
if arg is None:
fatal_error(arg, "@willFree(...) requires a parameter name or its 0-based position", "LifetimeError")
fatal_error(arg, "an @life(...) tag requires a parameter name or its 0-based position", "LifetimeError")
return -1
# FunctionDef.args 的字段静态类型是 AST*(见 includes/ast/stmts.vp),必须先转成 Arguments*
# 才能再取 .args 列表;直接写 fn.args.args 会去 AST 类查 'args' 触发 MemberLookupFailFast。
if fn.args is None:
fatal_error(arg, "@willFree(...) cannot be used here: the definition has no parameter list", "LifetimeError")
fatal_error(arg, "an @life(...) tag cannot be used here: the definition has no parameter list", "LifetimeError")
return -1
fargs: ast.Arguments* = ast.Arguments*(fn.args)
if fargs is None or fargs.args is None:
fatal_error(arg, "@willFree(...) cannot be used here: the definition has no parameter list", "LifetimeError")
fatal_error(arg, "an @life(...) tag cannot be used here: the definition has no parameter list", "LifetimeError")
return -1
pl: list[ast.AST*]* = fargs.args
pn: sizet = len(pl)
@@ -480,18 +607,18 @@ def _WillFreeArgIndex(fn: ast.FunctionDef*, arg: ast.AST*) -> int:
if pa is None or pa.kind() != ast.ASTKind.Arg: continue
aa: ast.Arg* = ast.Arg*(pa)
if aa.arg is not None and string.strcmp(aa.arg, nma.id) == 0: return int(k)
fatal_error(arg, f"@willFree({nma.id}): '{nma.id}' is not a parameter of this function", "LifetimeError")
fatal_error(arg, f"@life tag argument '{nma.id}': not a parameter of this function", "LifetimeError")
return -1
if arg.kind() != ast.ASTKind.Constant:
fatal_error(arg, "@willFree(...) accepts only a parameter name or an integer position", "LifetimeError")
fatal_error(arg, "an @life(...) tag accepts only a parameter name or an integer position", "LifetimeError")
return -1
ca: ast.Constant* = ast.Constant*(arg)
if ca.const_kind != ast.CONST_INT:
fatal_error(arg, "@willFree(...) accepts only a parameter name or an integer position", "LifetimeError")
fatal_error(arg, "an @life(...) tag accepts only a parameter name or an integer position", "LifetimeError")
return -1
idx: int = int(ca.int_val)
if idx < 0 or idx >= int(pn):
fatal_error(arg, f"@willFree({idx}): parameter position out of range (the function has {int(pn)} parameter(s))", "LifetimeError")
fatal_error(arg, f"@life tag position {idx}: out of range (the function has {int(pn)} parameter(s))", "LifetimeError")
return -1
return idx
+5 -5
View File
@@ -434,11 +434,11 @@ novirtual class StmtGenCifMixin:
# f-string escape (return f"..."): its buffer lives on this frame's stack and dies on
# return; copy it out explicitly (GenApi.escape_fstring_copy) before returning it.
GenApi.fail_if_fstring_escape(rt.value, "function return value")
# 栈地址逃逸:`return &local`(显式取址)与 `return arr`(返回类型是指针时的数组退让,
# 如 `-> char*`)都交出本帧栈地址;`return arr` 且返回类型为同型聚合时按值拷贝,不在此列。
rv_rt_sa: mlirlite.LLVMType* = None
if trans._cur_func is not None: rv_rt_sa = mlirlite.function_get_ret_ty(trans._cur_func)
GenApi.fail_if_stack_addr_escape(rt.value, "a return value", trans, rv_rt_sa)
# A2(§14.3 第 6 条):此处**不再**对 `return &local` / `return arr`(指针位置的数组退让)
# 直接 fail-fast。返回值去向改由**调用点**按被调者的 `@life(ret(...))` 契约判定(见
# AssignGenMixin.life_of_expr 的 ret_kind 分支):定义端无法区分"交出本帧"与"借出调用方的
# 存储",一律拦会误报 `return <借用形参>`。必须在 `ret_kind` 契约(同批)落地后移除,
# 否则未声明的函数会静默漏检。
# return a, b → pack an anonymous struct value (multi-return, return type tuple[T1,T2])
if rt.value.kind() == ast.ASTKind.Tuple:
rtp: ast.Tuple* = ast.Tuple*(rt.value)
+56 -25
View File
@@ -174,9 +174,9 @@ novirtual class StructGenMixin:
# 命中该位时把**首个**造成逃逸的写入点行号写进 EscLeakLine。-1 = 不采集(登记/落盘路径恒为 -1)。
EscLeakWant: int
EscLeakLine: int
# ── 声明式指令表(`@life(escape(...))` / `@life(noescape(...))`)──
# 3 槽/项:[名哈希, 强制逃逸位, 强制不逃逸位]。调用点在**所有推断与裁剪之后**最后套用,
# 是不可被推翻的契约("unsafe 但强制")。每文件重置计数。
# ── 声明式指令表(`@life(escape(...))` / `@life(noescape(...))` / `@life(ret(...))`)──
# 4 槽/项:[名哈希, 强制逃逸位, 强制不逃逸位, ret_kind(A2,见 GenApi.RET_KIND_*)]。
# 调用点在**所有推断与裁剪之后**最后套用,是不可被推翻的契约("unsafe 但强制")。每文件重置计数。
EscDirTable: sizet*
EscDirCount: int
# §13 NLL:当前正在翻译的函数定义(谓词 NllFrameConfined 要拿它的函数体做遍历)。
@@ -369,10 +369,10 @@ 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):同样只分配一次,每文件重置计数
# 声明式指令表(escape/noescape/ret):同样只分配一次,每文件重置计数
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.EscDirTable = sizet*(esc_pool.alloc(esc_cap * 32))
if self.EscDirTable is not None: string.memset(self.EscDirTable, 0, esc_cap * 32)
self.EscDirCount = 0
self.EscFuncCount = 0
self.EscConstEnv = None
@@ -542,7 +542,7 @@ novirtual class StructGenMixin:
if idx < 0: return 0
return int(self.EscTable[idx * 3 + 2])
# ---- 声明式指令(@life(escape/noescape)):调用点的**最终裁决**表 ----
# ---- 声明式指令(@life(escape/noescape/ret)):调用点的**最终裁决**表 ----
# 与 EscTable 分开存:EscTable 的掩码是"推断结果"(能被常量特化/同命裁剪改动),
# 指令是"契约"(不能被任何推断推翻),调用点在所有推断之后最后套用一次。
def _DirLookup(self, name: str) -> int:
@@ -551,7 +551,7 @@ novirtual class StructGenMixin:
h: sizet = self._NameHash(name)
i: int = 0
while i < self.EscDirCount:
if self.EscDirTable[i * 3] == h: return i
if self.EscDirTable[i * 4] == h: return i
i += 1
return -1
@@ -559,13 +559,38 @@ novirtual class StructGenMixin:
"""`@life(escape(...))` 强制逃逸位掩码;0 = 无该标注"""
idx: int = self._DirLookup(name)
if idx < 0: return 0
return int(self.EscDirTable[idx * 3 + 1])
return int(self.EscDirTable[idx * 4 + 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])
return int(self.EscDirTable[idx * 4 + 2])
def get_ret_kind(self, name: str) -> int:
"""`@life(ret(...))` 返回值生命期契约编码(GenApi.RET_KIND_*);0 = 无声明"""
idx: int = self._DirLookup(name)
if idx < 0: return 0
return int(self.EscDirTable[idx * 4 + 3])
# get_ret_kind_method - 方法版 `@life(ret(...))`:查 "Class.method" 并沿 MRO 回退。
def get_ret_kind_method(self, pool: memhub.MemBuddy*, class_name: str, method_name: str) -> int:
if method_name is None or class_name is None: return 0
key_q: str = self._EscKey2(pool, class_name, method_name)
if key_q is not None:
r_q: int = self.get_ret_kind(key_q)
if r_q != 0: return r_q
mro: sizet* = self.get_mro_chain(pool, class_name)
mi2: int = 1
while mro is not None and mro[mi2] != 0:
anc_m: str = rawptr(mro[mi2])
if anc_m is not None and string.strcmp(anc_m, class_name) != 0:
key_a: str = self._EscKey2(pool, anc_m, method_name)
if key_a is not None:
r_a: int = self.get_ret_kind(key_a)
if r_a != 0: return r_a
mi2 += 1
return 0
def _DirForceOf(self, name: str, noescape: int) -> int:
"""按方向取指令位:noescape!=0 → 强制不逃逸槽,否则 → 强制逃逸槽"""
@@ -680,17 +705,21 @@ novirtual class StructGenMixin:
# 显式标注(声明式契约)**在扫描之后**叠加:escape 无条件置位、noescape 无条件清位。
# 顺序是关键:常量折叠发生在扫描里,所以 `escape(p)` 不会被"死分支"折掉 —— 契约的本意
# 正是"编译器看不见的路径";`noescape(p)` 也不会被任何推导重新置位。
# 输出缓冲借用 box[3]/box[4](= 槽 68/69,此刻 env 已用完),不依赖额外 scratch;
# 这两个槽同时把**声明式指令**交给调用方(_RegisterFuncEscapeSummary 落 EscDirTable →
# 输出缓冲借用 box[3]/box[4]/box[5](= 槽 68/69/70,此刻 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])
if ef_m != 0 or ne_m != 0:
box[0] = (box[0] | sizet(ef_m)) & (~sizet(ne_m))
# 强制逃逸同样算**硬**位:调用点不能再按"同命"把它裁掉
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])
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))
# A2(§14):`@life(ret(...))` 的返回值生命期契约落在 box[5](escape/noescape 占 box[3]/box[4]);
# 与它们同一次登记进 EscDirTable / 落 .vpi 的 escr=。0 = 无声明。
life_out: sizet* = sizet*(uint64(box) + 5 * 8)
GenApi.compute_life_anno(fd, life_out)
return 0
# ============================================================
@@ -1659,14 +1688,16 @@ 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)。
# 声明式指令(@life(escape/noescape/ret))单独落表:调用点在所有推断/裁剪之后最后套用。
# box[3]/box[4] 由 compute_escape_summary 借给 compute_escape_directives 当输出(无标注时恒 0),
# box[5] 是 compute_life_anno 的 ret_kind。
if self.EscDirTable is not None and self.EscDirCount < 1024:
if box[3] != 0 or box[4] != 0:
if box[3] != 0 or box[4] != 0 or box[5] != 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.EscDirTable[d_s * 4 + 0] = sizet(self._NameHash(key))
self.EscDirTable[d_s * 4 + 1] = box[3]
self.EscDirTable[d_s * 4 + 2] = box[4]
self.EscDirTable[d_s * 4 + 3] = box[5]
self.EscDirCount = d_s + 1
return 0