From 6da8e2e095bd2e68a2b2dfc730628d49236a9179 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Fri, 9 Oct 2026 22:58:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(gen):=20=E8=90=BD=E5=9C=B0=20@life(ret(...?= =?UTF-8?q?))=20=E8=BF=94=E5=9B=9E=E5=80=BC=E7=94=9F=E5=91=BD=E6=9C=9F?= =?UTF-8?q?=E5=A5=91=E7=BA=A6=EF=BC=88A2=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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=(Converter 写、VpiReader 读 _g_vpi_esd_r)。 - AssignGenMixin.life_of_expr(Call):@life(ret(...)) 声明优先于任何推断; frame/heap/static/borrows=none 直接映射,borrows=x 取对应实参生命期, 方法调用按接收者静态类型解析类名。 - StmtGenCifMixin:同批移除定义端 return <本帧地址> 的 fail-fast, 返回值去向改由调用点按被调者契约判定。 --- App/lib/Stub/StubGen/Converter.vp | 9 +- App/lib/Stub/StubGen/VpiReader.vp | 36 ++++-- App/lib/core/Gen/AssignGenMixin.vp | 105 +++++++++++++++- App/lib/core/Gen/Call/CallGenBase.vp | 13 ++ App/lib/core/Gen/GenApi.vp | 149 +++++++++++++++++++++-- App/lib/core/Gen/Stmt/StmtGenCifMixin.vp | 10 +- App/lib/core/Gen/StructGenMixin.vp | 81 ++++++++---- 7 files changed, 347 insertions(+), 56 deletions(-) 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