diff --git a/App/lib/core/Gen/AssignGenMixin.vp b/App/lib/core/Gen/AssignGenMixin.vp index cd21b32..4addd15 100644 --- a/App/lib/core/Gen/AssignGenMixin.vp +++ b/App/lib/core/Gen/AssignGenMixin.vp @@ -1057,6 +1057,24 @@ novirtual class AssignGenMixin: # _is_ptr_element_annotation - check whether an annotation is bytes* or str* # bytes*/str* => 8-byte element stride (i8**); char*/int8* => 1-byte stride. + def _RefKindOf(self, annot: ast.AST*) -> int: + """OBS reference kind of an annotation: 0=none, 1=`&T` (read-only), 2=`&mut T`.""" + if annot is None: return 0 + if GenApi.strict_obs() == 0: return 0 + return self.Gen.TypeGenResolve.ref_kind(annot) + + def _BorrowOwnerName(self, expr: ast.AST*) -> str: + """Borrowed variable of `r: &T = x` / `r: &T = &x`; None when the source is not a binding.""" + if expr is None: return None + if expr.kind() == ast.ASTKind.Name: + return (ast.Name*)(expr).id + if expr.kind() == ast.ASTKind.UnaryOp: + uo_b: ast.UnaryOp* = ast.UnaryOp*(expr) + if uo_b.op == ast.OpKind.AddrOf and uo_b.operand is not None \ + and uo_b.operand.kind() == ast.ASTKind.Name: + return (ast.Name*)(uo_b.operand).id + return None + def _is_ptr_element_annotation(self, annot: ast.AST*) -> int: """Return 1 if the annotation is a bytes* or str* form.""" if annot is None: return 0 @@ -1739,6 +1757,9 @@ novirtual class AssignGenMixin: # 出口只释放 q 一次。不转移的话,`p = None` 会把 q 指向的对象提前释放 → q 悬垂。 if src_slot is not None: if self.Gen.VarGen._SlifeOwned(src_slot) != 0: + # A reference borrows: no ownership transfer, no shadow ownership. + if target_name is not None and self.Gen.VarGen.var_ref_kind(trans.SymTab, target_name) != 0: + return 0 # The source owns a heap pointer: a value-typed target cannot receive it # implicitly (same rule as `new T()` into `T`). if target_slot.Ty is None or target_slot.Ty.Pointee == 0 or self.Gen.ExprGenBase.is_ptr_type(target_slot.Ty.Pointee) == 0: @@ -1755,7 +1776,9 @@ novirtual class AssignGenMixin: # Move semantics: the source no longer owns the value. Applies to # explicit `own` bindings in any file, and to every OBS move. if trans.StrictOBS != 0 or (target_name is not None and self.Gen.VarGen.var_is_own(trans.SymTab, target_name) != 0): - self.Gen.VarGen._FreedMark(src_slot) + # A reference borrows the object; it never takes ownership. + if target_name is None or self.Gen.VarGen.var_ref_kind(trans.SymTab, target_name) == 0: + self.Gen.VarGen._FreedMark(src_slot) return 1 self._SlifeDropOwned(pool, builder, mod, trans, target_slot) return 0 @@ -2702,6 +2725,13 @@ novirtual class AssignGenMixin: alloca: mlirlite.Value* = self.Gen.VarGen._alloca_at_entry(builder, var_ty) if alloca is not None: if self.Gen.VarGen.define_var(trans.SymTab, nm.id, alloca) == 0: + # OBS reference: remember the read-only / mutable kind and the borrowed owner. + rk_decl: int = self._RefKindOf(annot) + if rk_decl != 0: + self.Gen.VarGen.set_var_ref_kind(trans.SymTab, nm.id, rk_decl) + owner_b: str = self._BorrowOwnerName(aa.value) + if owner_b is not None: + self.Gen.StmtGen._BorrowAdd(trans, owner_b, nm.id) # Store the annotation's class name; pass pool so generic Subscript yields the mangled name if annot is not None: cls_nm_ps: str = self.Gen.TypeGenResolve.extract_class_name_from_annotation(annot, trans._imported_modules, pool, trans._from_imports) @@ -3580,9 +3610,21 @@ novirtual class AssignGenMixin: return 0 if self.Gen.VarGen.var_is_const(ve_c) != 0: self.const_assign_fail(tgt_c, "assignment", nm_c.id, self.Gen.VarGen.var_annot_class_name(ve_c)) + # Reassigning a reference drops its old borrow (a fresh borrow is re-registered). + if self.Gen.VarGen.var_ref_kind(trans.SymTab, nm_c.id) != 0: + self.Gen.StmtGen._BorrowDrop(trans, nm_c.id) + owner_rb: str = self._BorrowOwnerName(asgn.value) + if owner_rb is not None: + self.Gen.StmtGen._BorrowAdd(trans, owner_rb, nm_c.id) elif tgt_c.kind() == ast.ASTKind.Attribute: at_c: ast.Attribute* = ast.Attribute*(tgt_c) if at_c.attr is None: continue + # OBS: writing through an immutable `&T` reference is forbidden. + if at_c.value is not None and at_c.value.kind() == ast.ASTKind.Name: + rf_bn: ast.Name* = ast.Name*(at_c.value) + if rf_bn.id is not None and self.Gen.VarGen.var_ref_kind(trans.SymTab, rf_bn.id) == 1: + self.Gen.TypeGenErr.fatal_error(tgt_c, f"cannot assign '.{at_c.attr}' through the immutable reference '{rf_bn.id}': declare it as `&mut T` to mutate the value", "TypeError") + return 0 if self.Gen.ExprGenAttr.attr_field_is_const(pool, trans, tgt_c) != 0: self.const_assign_fail(tgt_c, "field assignment", at_c.attr, self.Gen.ExprGenAttr.attr_field_decl_src(pool, trans, tgt_c)) if trans.StrictOBS != 0 and asgn.value is not None: @@ -3598,6 +3640,9 @@ novirtual class AssignGenMixin: if sub_c.value.kind() == ast.ASTKind.Name: nm_c2: ast.Name* = ast.Name*(sub_c.value) if nm_c2.id is None: continue + if self.Gen.VarGen.var_ref_kind(trans.SymTab, nm_c2.id) == 1: + self.Gen.TypeGenErr.fatal_error(tgt_c, f"cannot assign through the immutable reference '{nm_c2.id}': declare it as `&mut T` to mutate the value", "TypeError") + return 0 ve_c2: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm_c2.id) if self.Gen.VarGen.var_is_const(ve_c2) != 0: self.const_assign_fail(tgt_c, "element assignment", nm_c2.id, self.Gen.VarGen.var_annot_class_name(ve_c2)) diff --git a/App/lib/core/Gen/Func/FuncGenBase.vp b/App/lib/core/Gen/Func/FuncGenBase.vp index 8b1e618..947169a 100644 --- a/App/lib/core/Gen/Func/FuncGenBase.vp +++ b/App/lib/core/Gen/Func/FuncGenBase.vp @@ -436,6 +436,9 @@ novirtual class FuncGenBase(CallGenBase.CallGenBase): alloca: mlirlite.Value* = mlirlite.build_alloca(builder, param_ty2) if alloca is None: continue self.Gen.VarGen.define_var(trans.SymTab, arg2.arg, alloca) + if GenApi.strict_obs() != 0 and arg2.annotation is not None: + rk_p: int = self.Gen.TypeGenResolve.ref_kind(arg2.annotation) + if rk_p != 0: self.Gen.VarGen.set_var_ref_kind(trans.SymTab, arg2.arg, rk_p) # Store the raw annotation class name (method-call detection falls back from Ptr(i8) to the class). if arg2.annotation is not None: cls_nm: str = self.Gen.TypeGenResolve.extract_class_name_from_annotation(arg2.annotation, trans._imported_modules, pool, trans._from_imports) diff --git a/App/lib/core/Gen/Func/FuncGenMixin.vp b/App/lib/core/Gen/Func/FuncGenMixin.vp index ad4645a..ecd0e6d 100644 --- a/App/lib/core/Gen/Func/FuncGenMixin.vp +++ b/App/lib/core/Gen/Func/FuncGenMixin.vp @@ -158,6 +158,9 @@ novirtual class FuncGenMixin(CallGenBase.CallGenBase): p_alloca: mlirlite.Value* = mlirlite.build_alloca(func_builder, pty2_n) if p_alloca is not None: self.Gen.VarGen.define_var(trans.SymTab, arg_p.arg, p_alloca) + if GenApi.strict_obs() != 0 and arg_p.annotation is not None: + rk_np: int = self.Gen.TypeGenResolve.ref_kind(arg_p.annotation) + if rk_np != 0: self.Gen.VarGen.set_var_ref_kind(trans.SymTab, arg_p.arg, rk_np) if arg_p.annotation is not None: cls_nm_p: str = self.Gen.TypeGenResolve.extract_class_name_from_annotation(arg_p.annotation, imported_modules, pool, trans._from_imports) if cls_nm_p is not None: self.Gen.VarGen.set_var_annot_class_name(trans.SymTab, arg_p.arg, cls_nm_p) diff --git a/App/lib/core/Gen/GenTypes.vp b/App/lib/core/Gen/GenTypes.vp index 772c67c..a4ab918 100644 --- a/App/lib/core/Gen/GenTypes.vp +++ b/App/lib/core/Gen/GenTypes.vp @@ -83,6 +83,9 @@ class VarEntry: # 0=native (no marker), 1=BigEndian, 2=LittleEndian. Appended at the end so existing field offsets # are unchanged; local loads/stores bswap when 1 (16/32/64-bit ints only). Mirrors FieldEntry.ByteOrder. ByteOrder: int + # Reference kind in an `__obs__` file: 0=not a reference, 1=`&T` (read-only), 2=`&mut T`. + # Appended at the end so existing field offsets are unchanged. + RefKind: int # 注:P2 的"已失效"状态**不放在这里** —— 给这个热结构加字段会改变布局、牵连整条变量注册 # 路径(现场:任何工程编译时都在"函数形参注册刚结束"处 access-violation)。改由 # VarGenMixin 的独立失效槽表 _g_freed_slots 保存(键 = 变量槽 alloca,槽本身唯一)。 diff --git a/App/lib/core/Gen/Stmt/StmtGenMixin.vp b/App/lib/core/Gen/Stmt/StmtGenMixin.vp index 51abc54..afbb48c 100644 --- a/App/lib/core/Gen/Stmt/StmtGenMixin.vp +++ b/App/lib/core/Gen/Stmt/StmtGenMixin.vp @@ -13,6 +13,57 @@ import lib.core.VLogger as VLogger import lib.core.Gen.Translator as HT import lib.core.CompilerFactory as CompilerFactory import lib.Projectrans.Config as Config + +# OBS borrow table: (function, owner, borrower) triples for `r: &T = x` / `r: &T = &x`. +# Entries are function-scoped; a borrow counts as alive while its reference variable is still +# declared in the current symbol table. +define BORROW_MAX = 128 +_g_borrow_funcs: sizet* = None +_g_borrow_owners: str* = None +_g_borrow_borrowers: str* = None +_g_borrow_count: int = 0 + +def _BorrowTableAdd(func: mlirlite.Function*, owner: str, borrower: str) -> int: + global _g_borrow_funcs, _g_borrow_owners, _g_borrow_borrowers, _g_borrow_count + if func is None or owner is None or borrower is None: return 0 + if _g_borrow_funcs is None: + _g_borrow_funcs = sizet*(stdlib.malloc(BORROW_MAX * 8)) + _g_borrow_owners = str*(stdlib.malloc(BORROW_MAX * 8)) + _g_borrow_borrowers = str*(stdlib.malloc(BORROW_MAX * 8)) + if _g_borrow_funcs is None or _g_borrow_owners is None or _g_borrow_borrowers is None: return 0 + # Refresh an existing (func, owner, borrower) entry instead of growing the table. + i: int = 0 + while i < _g_borrow_count: + if _g_borrow_funcs[i] == sizet(func) and _g_borrow_borrowers[i] is not None \ + and string.strcmp(_g_borrow_borrowers[i], borrower) == 0: + _g_borrow_owners[i] = owner + return 1 + i += 1 + if _g_borrow_count >= BORROW_MAX: return 0 + _g_borrow_funcs[_g_borrow_count] = sizet(func) + _g_borrow_owners[_g_borrow_count] = owner + _g_borrow_borrowers[_g_borrow_count] = borrower + _g_borrow_count += 1 + return 1 + +def _BorrowTableDrop(func: mlirlite.Function*, borrower: str) -> int: + """Forget every borrow held by `borrower` (the reference was reassigned).""" + global _g_borrow_count + if func is None or borrower is None or _g_borrow_funcs is None: return 0 + i: int = 0 + while i < _g_borrow_count: + if _g_borrow_funcs[i] == sizet(func) and _g_borrow_borrowers[i] is not None \ + and string.strcmp(_g_borrow_borrowers[i], borrower) == 0: + j: int = i + while j + 1 < _g_borrow_count: + _g_borrow_funcs[j] = _g_borrow_funcs[j + 1] + _g_borrow_owners[j] = _g_borrow_owners[j + 1] + _g_borrow_borrowers[j] = _g_borrow_borrowers[j + 1] + j += 1 + _g_borrow_count -= 1 + continue + i += 1 + return 1 import lib.core.Gen.Stmt.StmtGenDeclMixin as StmtGenDeclMixin import lib.core.Gen.DebugInfoGen as DebugInfoGen @@ -24,6 +75,31 @@ import lib.core.Gen.DebugInfoGen as DebugInfoGen novirtual class StmtGenMixin(StmtGenDeclMixin.StmtGenDeclMixin): """Statement/flow translation Mixin (top level, extends Decl)""" + def _BorrowAdd(self, trans: HT.Translator*, owner: str, borrower: str) -> int: + """Record that reference `borrower` refers to `owner` inside the current function.""" + if trans is None: return 0 + return _BorrowTableAdd(trans._cur_func, owner, borrower) + + def _BorrowDrop(self, trans: HT.Translator*, borrower: str) -> int: + """Forget the borrows held by `borrower` (it was reassigned).""" + if trans is None: return 0 + return _BorrowTableDrop(trans._cur_func, borrower) + + def _BorrowLiveBorrower(self, trans: HT.Translator*, owner: str) -> str: + """Name of a still-declared reference that borrows `owner` (None when none).""" + global _g_borrow_count + if trans is None or owner is None or _g_borrow_funcs is None: return None + i: int = 0 + while i < _g_borrow_count: + if _g_borrow_funcs[i] == sizet(trans._cur_func): + own_n: str = _g_borrow_owners[i] + if own_n is not None and string.strcmp(own_n, owner) == 0: + b_n: str = _g_borrow_borrowers[i] + if b_n is not None and self.Gen.VarGen.lookup_var_entry(trans.SymTab, b_n) is not None: + return b_n + i += 1 + return None + def translate_stmt(self, trans: HT.Translator*, node: ast.AST*) -> int: """Translate a single statement with a depth guard; returns the number of new variables""" if trans is not None: @@ -132,6 +208,15 @@ novirtual class StmtGenMixin(StmtGenDeclMixin.StmtGenDeclMixin): if self.Gen.VarGen._FreedCheck(del_slot) != 0: self.Gen.TypeGenErr.fatal_error(tgt, f"cannot `del {nm_del.id}`: it was already released", "LifetimeError") continue + # OBS references: a borrow is not an owner, and the owner cannot be released while + # a reference to it is still alive. + if self.Gen.VarGen.var_ref_kind(trans.SymTab, nm_del.id) != 0: + self.Gen.TypeGenErr.fatal_error(tgt, f"cannot `del {nm_del.id}`: it is a reference ('&T' / '&mut T'), not an owner", "LifetimeError") + continue + live_b: str = self._BorrowLiveBorrower(trans, nm_del.id) + if live_b is not None: + self.Gen.TypeGenErr.fatal_error(tgt, f"cannot `del {nm_del.id}`: the reference '{live_b}' is still in scope; reassign it (or delete after it is gone) first", "LifetimeError") + continue del_var_ty: mlirlite.LLVMType* = del_slot.Ty.Pointee del_struct_ty: mlirlite.LLVMType* = None if self.Gen.ExprGenBase.is_ptr_type(del_var_ty) != 0: del_struct_ty = del_var_ty.Pointee diff --git a/App/lib/core/Gen/Type/TypeGenResolve.vp b/App/lib/core/Gen/Type/TypeGenResolve.vp index de6f0ff..393fb82 100644 --- a/App/lib/core/Gen/Type/TypeGenResolve.vp +++ b/App/lib/core/Gen/Type/TypeGenResolve.vp @@ -1366,7 +1366,7 @@ novirtual class TypeGenResolve(TypeGenBase.TypeGenBase): if ei >= 3 and tr[ei - 3] == '_' and tr[ei - 2] == 'b' and tr[ei - 1] == 'e': return 1 return 0 def _ref_marker_name_mode(self, node: ast.AST*) -> int: - """Syntactic value-reference marker test (no import context): 0=none / 1=RefT / 2=TempRefT.""" + """Syntactic value-reference marker test (no import context): 0=none / 1=RefT / 2=TempRefT / 3=MutRefT.""" if node is None: return 0 if node.kind() != ast.ASTKind.Subscript: return 0 sub_r: ast.Subscript* = ast.Subscript*(node) @@ -1376,13 +1376,33 @@ novirtual class TypeGenResolve(TypeGenBase.TypeGenBase): if nm is None: return 0 if string.strcmp(nm, "RefT") == 0: return 1 if string.strcmp(nm, "TempRefT") == 0: return 2 + if string.strcmp(nm, "MutRefT") == 0: return 3 return 0 def ref_param_mode(self, node: ast.AST*, imported_modules: str) -> int: - """Value-reference mode of a parameter annotation: 0=none / 1=ref (&T) / 2=temp (&temp T).""" - return self._ref_marker_name_mode(node) + """Value-reference mode of a parameter annotation: 0=none / 1=ref (&T) / 2=temp (&temp T). + + In an `__obs__` file `&T` / `&mut T` are first-class reference values, so no call-site + auto-address is applied and the parameter is a plain pointer. + """ + if GenApi.strict_obs() != 0: return 0 + m: int = self._ref_marker_name_mode(node) + if m == 3: return 0 + return m + def ref_kind(self, node: ast.AST*) -> int: + """Reference kind of an annotation: 0=none / 1=`&T` / 2=`&mut T`.""" + m: int = self._ref_marker_name_mode(node) + if m == 1: return 1 + if m == 3: return 2 + return 0 def reject_ref_marker_position(self, node: ast.AST*, imported_modules: str, where: str) -> int: - """Fail fast when a value-reference marker (&T / &temp T) is used outside a parameter position.""" - if self.ref_param_mode(node, imported_modules) == 0: return 0 + """Fail fast when a reference marker is used where it is not allowed.""" + mode: int = self._ref_marker_name_mode(node) + if mode == 0: return 0 + # OBS references are ordinary types in every annotation position. + if GenApi.strict_obs() != 0: return 0 + if mode == 3: + self.Gen.TypeGenErr.fatal_error(node, "mutable references (`&mut T`) require `from __future__ import __obs__`", "TypeError") + return 1 self.Gen.TypeGenErr.fatal_error(node, f"value-reference parameter annotation '&T' / '&temp T' is only allowed in function parameter position, not for {where}", "TypeError") return 1 def _is_cattr_marker(self, node: ast.AST*) -> int: @@ -1417,7 +1437,8 @@ novirtual class TypeGenResolve(TypeGenBase.TypeGenBase): or string.strcmp(sw_nm.id, "StaticT") == 0 or string.strcmp(sw_nm.id, "ExportT") == 0 \ or string.strcmp(sw_nm.id, "ExternT") == 0 or string.strcmp(sw_nm.id, "State") == 0 \ or string.strcmp(sw_nm.id, "DefineT") == 0 or string.strcmp(sw_nm.id, "RefT") == 0 \ - or string.strcmp(sw_nm.id, "TempRefT") == 0 or string.strcmp(sw_nm.id, "AutoPtrT") == 0): + or string.strcmp(sw_nm.id, "TempRefT") == 0 or string.strcmp(sw_nm.id, "MutRefT") == 0 \ + or string.strcmp(sw_nm.id, "AutoPtrT") == 0): if sub_v.slice is not None: return self._is_cvoid_marker(sub_v.slice, imported_modules) return 0 def annotation_to_str(self, annot: ast.AST*) -> str: @@ -1465,10 +1486,13 @@ novirtual class TypeGenResolve(TypeGenBase.TypeGenBase): viperlib.snprintf(cp_buf, string.strlen(inner_cp) + 2, "%s*", inner_cp) return cp_buf return "" - if string.strcmp(sv_nm.id, "RefT") == 0 or string.strcmp(sv_nm.id, "TempRefT") == 0: + if string.strcmp(sv_nm.id, "RefT") == 0 or string.strcmp(sv_nm.id, "TempRefT") == 0 \ + or string.strcmp(sv_nm.id, "MutRefT") == 0: inner_rf: str = self.annotation_to_str(sub.slice) if inner_rf is not None: - rf_prefix: str = "&temp " if string.strcmp(sv_nm.id, "TempRefT") == 0 else "&" + rf_prefix: str = "&" + if string.strcmp(sv_nm.id, "TempRefT") == 0: rf_prefix = "&temp " + elif string.strcmp(sv_nm.id, "MutRefT") == 0: rf_prefix = "&mut " rf_buf: str = stdlib.malloc(string.strlen(inner_rf) + string.strlen(rf_prefix) + 1) if rf_buf is not None: viperlib.snprintf(rf_buf, string.strlen(inner_rf) + string.strlen(rf_prefix) + 1, "%s%s", rf_prefix, inner_rf) @@ -1610,7 +1634,8 @@ novirtual class TypeGenResolve(TypeGenBase.TypeGenBase): or string.strcmp(rp_nm.id, "StaticT") == 0 or string.strcmp(rp_nm.id, "ExportT") == 0 \ or string.strcmp(rp_nm.id, "ExternT") == 0 or string.strcmp(rp_nm.id, "State") == 0 \ or string.strcmp(rp_nm.id, "DefineT") == 0 or string.strcmp(rp_nm.id, "RefT") == 0 \ - or string.strcmp(rp_nm.id, "TempRefT") == 0 or string.strcmp(rp_nm.id, "AutoPtrT") == 0: + or string.strcmp(rp_nm.id, "TempRefT") == 0 or string.strcmp(rp_nm.id, "MutRefT") == 0 \ + or string.strcmp(rp_nm.id, "AutoPtrT") == 0: if sub.slice is not None: return self.extract_class_name_from_annotation(sub.slice, imported_modules, pool, from_imports) return None # When recursing into base, from_imports must be passed down too, else the mangled @@ -2313,7 +2338,8 @@ novirtual class TypeGenResolve(TypeGenBase.TypeGenBase): or string.strcmp(gen_nm.id, "DefineT") == 0 or string.strcmp(gen_nm.id, "AutoPtrT") == 0: return self.resolve_annotation_type(pool, sub.slice, imported_modules, from_imports, trans, depth + 1) # Value-reference parameters use the pointer ABI: &T / &temp T -> T*. - if string.strcmp(gen_nm.id, "RefT") == 0 or string.strcmp(gen_nm.id, "TempRefT") == 0: + if string.strcmp(gen_nm.id, "RefT") == 0 or string.strcmp(gen_nm.id, "TempRefT") == 0 \ + or string.strcmp(gen_nm.id, "MutRefT") == 0: ref_inner: mlirlite.LLVMType* = self.resolve_annotation_type(pool, sub.slice, imported_modules, from_imports, trans, depth + 1) if ref_inner is None: return None return mlirlite.Ptr(pool, ref_inner) diff --git a/App/lib/core/Gen/VarGenMixin.vp b/App/lib/core/Gen/VarGenMixin.vp index a920b2f..951ab6a 100644 --- a/App/lib/core/Gen/VarGenMixin.vp +++ b/App/lib/core/Gen/VarGenMixin.vp @@ -449,6 +449,21 @@ novirtual class VarGenMixin: if symtab is None or name is None: return 0 return self._OwnCheck(self.lookup_var(symtab, name)) + def set_var_ref_kind(self, symtab: GenTypes.SymbolTable*, name: str, kind: int) -> int: + """Record the OBS reference kind of a variable: 0=none, 1=`&T`, 2=`&mut T`.""" + if symtab is None or name is None: return 0 + ve: GenTypes.VarEntry* = self.lookup_var_entry(symtab, name) + if ve is None: return 0 + ve.RefKind = kind + return 1 + + def var_ref_kind(self, symtab: GenTypes.SymbolTable*, name: str) -> int: + """OBS reference kind of the variable: 0=none, 1=`&T` (read-only), 2=`&mut T`.""" + if symtab is None or name is None: return 0 + ve: GenTypes.VarEntry* = self.lookup_var_entry(symtab, name) + if ve is None: return 0 + return ve.RefKind + # ---- P2 willFree:失效槽表的控制流汇合(与 life_capture/life_restore/life_join 同点位) ---- # 背景:失效表是"平坦、单调"的全局表(_FreedMark 只插入),原本无分支合流语义, # 于是"分支体内 free(x)"会把失效态泄漏到合流之后 → `if c: free(p); return` 之后的