diff --git a/App/lib/Stub/StubGen/Converter.vp b/App/lib/Stub/StubGen/Converter.vp
index 0b79475..6f58951 100644
--- a/App/lib/Stub/StubGen/Converter.vp
+++ b/App/lib/Stub/StubGen/Converter.vp
@@ -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")
diff --git a/App/lib/Stub/StubGen/VpiReader.vp b/App/lib/Stub/StubGen/VpiReader.vp
index bfd7a14..a534202 100644
--- a/App/lib/Stub/StubGen/VpiReader.vp
+++ b/App/lib/Stub/StubGen/VpiReader.vp
@@ -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
diff --git a/App/lib/core/Gen/AssignGenMixin.vp b/App/lib/core/Gen/AssignGenMixin.vp
index a6417b3..007844e 100644
--- a/App/lib/core/Gen/AssignGenMixin.vp
+++ b/App/lib/core/Gen/AssignGenMixin.vp
@@ -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:
diff --git a/App/lib/core/Gen/Call/CallGenBase.vp b/App/lib/core/Gen/Call/CallGenBase.vp
index cc632e9..e9fd05d 100644
--- a/App/lib/core/Gen/Call/CallGenBase.vp
+++ b/App/lib/core/Gen/Call/CallGenBase.vp
@@ -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=无摘要)。
diff --git a/App/lib/core/Gen/GenApi.vp b/App/lib/core/Gen/GenApi.vp
index 03c1f18..f83bf8b 100644
--- a/App/lib/core/Gen/GenApi.vp
+++ b/App/lib/core/Gen/GenApi.vp
@@ -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=` 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=|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=` / `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=` / `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
diff --git a/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp b/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp
index a84e9a6..bfa0a7a 100644
--- a/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp
+++ b/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp
@@ -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)
diff --git a/App/lib/core/Gen/StructGenMixin.vp b/App/lib/core/Gen/StructGenMixin.vp
index b50eac6..0d99be0 100644
--- a/App/lib/core/Gen/StructGenMixin.vp
+++ b/App/lib/core/Gen/StructGenMixin.vp
@@ -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