feat(obs): first-class &T / &mut T references

In __obs__ files references are ordinary pointer values (no call-site auto-address), &T is read-only and &mut T writable. A reference borrows its owner: the owner cannot be del-ed while the reference variable is still in scope, references are never owners, and reassigning a reference drops the old borrow. Outside __obs__, &mut T is rejected.
This commit is contained in:
TermiNexus committed 2026-11-04 15:48:21 +08:00
1 parent dbee021cd1
commit b9d84c6bda
7 files changed
+191 -11

No files matched your search

+46 -1
View File
@@ -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))
+3
View File
@@ -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)
+3
View File
@@ -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)
+3
View File
@@ -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,槽本身唯一)。
+85
View File
@@ -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
+36 -10
View File
@@ -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 "<subscript>"
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)
+15
View File
@@ -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` 之后的