feat(gen): del obj reclaims a heap object (__del__ then free)

`del name` now destroys a class/struct instance:

- requires a pointer variable (T*) to a class/struct; value-typed T and
  non-struct types fail fast,
- rejects frame-allocated pointers (`new stack` / borrows) and repeat
  `del` of the same binding,
- calls `__del__` (MRO-aware, skipped for a null pointer) and then
  `free`s the object,
- cancels the automatic scope-exit release when the binding is a
  compiler-owned `slife` object, so the destructor and free still happen
  exactly once,
- marks the slot released: later reads, field access, subscript access
  and `&name` fail with a LifetimeError until the variable is rebound
  (rebinding clears the mark, as with willFree).

The released-variable diagnostic now names both causes (`del` or
`@life(willFree(...))`) and drops the obsolete `c.deref(p)` spelling.

Probe: hlife object prints `__del__` output once and exits 0; slife
object prints once (no second call at scope exit); field read/write and
plain use after `del` fail fast; rebound variable is usable again.
Self-host 110/110; ListTest, GenericTest3, PtrOpProbe, HashLibTest,
NewTest, NewLifetimeTest, StructTest and EscapeOkTest pass.
This commit is contained in:
TermiNexus committed 2026-11-02 15:15:20 +08:00
1 parent d9511a54ea
commit 5f981d7878
5 files changed
+85 -17

No files matched your search

+10 -2
View File
@@ -524,7 +524,11 @@ novirtual class ExprGenAttr(ExprGenBase.ExprGenBase):
case _:
nl_val_ptr_a: mlirlite.Value* = mlirlite.build_bitcast(builder, raw_nl_a, mlirlite.Ptr(pool, nl_ty_a))
if nl_val_ptr_a is not None: obj_ptr = mlirlite.build_load(builder, nl_ty_a, nl_val_ptr_a)
else: obj_ptr = self.Gen.VarGen.lookup_var(trans.SymTab, nm.id)
else:
obj_ptr = self.Gen.VarGen.lookup_var(trans.SymTab, nm.id)
if self.Gen.VarGen._FreedCheck(obj_ptr) != 0:
self.Gen.TypeGenErr.fatal_error(at.value, f"cannot use '{nm.id}': already released (by `del` or a @life(willFree(...)) call)", "LifetimeError")
return None
# non-Name path: translate the object value (which loads)
if obj_ptr is None:
@@ -938,7 +942,11 @@ novirtual class ExprGenAttr(ExprGenBase.ExprGenBase):
case _:
nl_val_ptr_a: mlirlite.Value* = mlirlite.build_bitcast(builder, raw_nl_a, mlirlite.Ptr(pool, nl_ty_a))
if nl_val_ptr_a is not None: obj_ptr = mlirlite.build_load(builder, nl_ty_a, nl_val_ptr_a)
else: obj_ptr = self.Gen.VarGen.lookup_var(trans.SymTab, nm.id)
else:
obj_ptr = self.Gen.VarGen.lookup_var(trans.SymTab, nm.id)
if self.Gen.VarGen._FreedCheck(obj_ptr) != 0:
self.Gen.TypeGenErr.fatal_error(at.value, f"cannot use '{nm.id}': already released (by `del` or a @life(willFree(...)) call)", "LifetimeError")
return None
# module attribute access (write path): sys._mbuddy = mb / os._win32._mbuddy = v → store to the module
# global. When obj is an imported module name (Name or nested Attribute path), find/create the global.
+1 -1
View File
@@ -891,7 +891,7 @@ novirtual class ExprGenBase:
if self._same_agg_ty(builder.Pool, slot_p2v, target_ty) != 0:
return mlirlite.build_load(builder, target_ty, val)
if self._same_agg_ty(builder.Pool, ctor_p2v, target_ty) == 0:
m_p2v: str = "implicit value/pointer conversion for non-basic types is forbidden (except construction materialization): a pointer type cannot be implicitly converted to a non-basic value (Foo* -> Foo); to dereference use c.deref(p) / *p explicitly, or cast explicitly"
m_p2v: str = "implicit value/pointer conversion for non-basic types is forbidden (except construction materialization): a pointer type cannot be implicitly converted to a non-basic value (Foo* -> Foo); to dereference use *p explicitly, or cast explicitly"
p2v_txt: str = GenApi.escape_fstring_copy(f"; source type {self._type_text(builder.Pool, val.Ty)} -> target value type {self._type_text(builder.Pool, target_ty)}")
self.Gen.TypeGenErr.fatal_error(self._value_node_of(node), f"{m_p2v}{p2v_txt}", "CastError")
return val
+7 -1
View File
@@ -42,7 +42,7 @@ novirtual class ExprGenName(ExprGenBase.ExprGenBase):
# 只在**读**路径检查;对同名变量重新赋值会清除失效态(见 AssignGenMixin)。
if trans is not None:
if self.Gen.VarGen._FreedCheck(self.Gen.VarGen.lookup_var(trans.SymTab, nm_id)) != 0:
self.Gen.TypeGenErr.fatal_error(node, f"cannot use '{nm_id}': already released by a @life(willFree(...)) call", "LifetimeError")
self.Gen.TypeGenErr.fatal_error(node, f"cannot use '{nm_id}': already released (by `del` or a @life(willFree(...)) call)", "LifetimeError")
# DefineT compile-time constant: return the constant (float -> double, int -> i32).
# Existing variables win over DefineT table entries injected from vpi stubs (lookup_var guard).
cdef_val_d: double = self.Gen.TypeGenCDef.lookup_cdefine_double(nm_id)
@@ -535,6 +535,9 @@ novirtual class ExprGenName(ExprGenBase.ExprGenBase):
if nl_elem_ty is not None: return mlirlite.build_load(builder, nl_elem_ty, nl_ptr_r)
return None
alloca: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, nm.id)
if self.Gen.VarGen._FreedCheck(alloca) != 0:
self.Gen.TypeGenErr.fatal_error(sub.value, f"cannot use '{nm.id}': already released (by `del` or a @life(willFree(...)) call)", "LifetimeError")
return None
if alloca is not None and alloca.Ty is not None:
if self.is_ptr_type(alloca.Ty) != 0:
pointee: mlirlite.LLVMType* = alloca.Ty.Pointee
@@ -844,6 +847,9 @@ novirtual class ExprGenName(ExprGenBase.ExprGenBase):
# cannot be used because its SSA number belongs to another function).
if GenApi.is_nonlocal_name(trans, nm.id) != 0: return self._nonlocal_subscript_ptr(builder, pool, trans, nm.id, idx_val)
alloca: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, nm.id)
if self.Gen.VarGen._FreedCheck(alloca) != 0:
self.Gen.TypeGenErr.fatal_error(sub.value, f"cannot use '{nm.id}': already released (by `del` or a @life(willFree(...)) call)", "LifetimeError")
return None
if alloca is not None and alloca.Ty is not None:
if self.is_ptr_type(alloca.Ty) != 0:
pointee: mlirlite.LLVMType* = alloca.Ty.Pointee
+1 -1
View File
@@ -918,7 +918,7 @@ novirtual class ExprGenOps(ExprGenBase.ExprGenBase):
if nm_ao.id is not None:
# P2 willFree:对已失效变量取地址 → fail-fast(与读取路径同一口径)
if self.Gen.VarGen._FreedCheck(self.Gen.VarGen.lookup_var(trans.SymTab, nm_ao.id)) != 0:
self.Gen.TypeGenErr.fatal_error(node, f"cannot take the address of '{nm_ao.id}': already released by a @life(willFree(...)) call", "LifetimeError")
self.Gen.TypeGenErr.fatal_error(node, f"cannot take the address of '{nm_ao.id}': already released (by `del` or a @life(willFree(...)) call)", "LifetimeError")
# self/cls object reference: return the object pointer itself (Ptr(Struct)), not the pointer variable's address (Ptr(Ptr(Struct))).
if string.strcmp(nm_ao.id, "self") == 0 or string.strcmp(nm_ao.id, "cls") == 0:
se_ao: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm_ao.id)
+66 -12
View File
@@ -121,20 +121,45 @@ novirtual class StmtGenMixin(StmtGenDeclMixin.StmtGenDeclMixin):
for ti in range(int(tn)):
tgt: ast.AST* = dnode.targets.get(ti)
if tgt is None: continue
# del obj -> __delete__ (matches TPC HandlesDelete.py; no-op if undefined)
# del obj -> reclaim a heap object: `__del__` first, then free; later uses fail
if tgt.kind() == ast.ASTKind.Name:
# §2.4 判据5:slife 对象由编译器独占(出口自动 __del__ + free),手动 del 会让析构跑两次
nm_del: ast.Name* = ast.Name*(tgt)
if nm_del is not None and nm_del.id is not None:
del_slot: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, nm_del.id)
if self.Gen.VarGen._SlifeOwned(del_slot) != 0:
self.Gen.TypeGenErr.fatal_error(tgt, f"cannot `del {nm_del.id}`: it is a compiler-owned `slife` object (freed automatically at scope exit); a manual del would double-free it", "LifetimeError")
del_obj: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, tgt, None, 0, trans)
if del_obj is not None:
del_st_nm: mlirlite.LLVMType* = self.Gen.StructGen.get_struct_type_from_value(del_obj)
if del_st_nm is not None:
del_cls_nm: str = self.Gen.StructGen.get_class_name_by_type(pool, del_st_nm)
if del_cls_nm is not None: self.Gen.ExprGenOps._try_named_dunder(pool, builder, mod, del_obj, "__delete__", trans)
if nm_del is None or nm_del.id is None: continue
del_slot: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, nm_del.id)
if del_slot is None or del_slot.Ty is None or del_slot.Ty.Pointee == 0:
self.Gen.TypeGenErr.fatal_error(tgt, f"cannot `del {nm_del.id}`: unknown variable", "TypeError")
continue
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
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
if del_struct_ty is None or del_struct_ty.kind() != mlirlite.LLVMType.Struct:
self.Gen.TypeGenErr.fatal_error(tgt, f"cannot `del {nm_del.id}`: del reclaims a heap object, so the variable must be a pointer (T*) to a class/struct", "TypeError")
continue
del_owned: int = self.Gen.VarGen._SlifeOwned(del_slot)
if self.Gen.VarGen.get_var_life(trans.SymTab, nm_del.id) == GenTypes.LIFE_FRAME and del_owned == 0:
self.Gen.TypeGenErr.fatal_error(tgt, f"cannot `del {nm_del.id}`: it points into the frame (`new stack` or a borrow), so there is no heap block to free", "LifetimeError")
continue
del_cur: mlirlite.Value* = mlirlite.build_load(builder, del_var_ty, del_slot)
if del_cur is None:
self.Gen.TypeGenErr.fatal_error(tgt, f"cannot `del {nm_del.id}`: the pointer value could not be loaded", "TypeError")
continue
self._emit_del_dtor(pool, builder, mod, trans, del_cur, del_var_ty, del_struct_ty)
if self.Gen.CallGenBase.find_func_in_module(mod, "free") is None:
self.Gen.TypeGenErr.fatal_error(tgt, "cannot `del`: the module has no declaration of the C library function 'free'", "TypeError")
continue
i8_ptr_del: mlirlite.LLVMType* = mlirlite.Ptr(pool, mlirlite.Int8(pool))
del_arg: mlirlite.Value* = del_cur
if del_cur.Ty is not None and uint64(del_cur.Ty) != uint64(i8_ptr_del):
del_arg = mlirlite.build_bitcast(builder, del_cur, i8_ptr_del)
if del_arg is not None: mlirlite.build_call(builder, "free", del_arg, 1, mlirlite.Void(pool), 0)
# cancel the automatic scope-exit release of a compiler-owned binding
if del_owned != 0:
del_sh: mlirlite.Value* = self.Gen.VarGen._SlifeShadow(del_slot)
if del_sh is not None: self.Gen.VarGen._SlifeNoteStore(pool, builder, del_sh, 0)
self.Gen.VarGen._FreedMark(del_slot)
# del obj.attr -> property deleter; falls back to __del__ (matches TPC)
elif tgt.kind() == ast.ASTKind.Attribute:
del_at: ast.Attribute* = ast.Attribute*(tgt)
@@ -176,6 +201,35 @@ novirtual class StmtGenMixin(StmtGenDeclMixin.StmtGenDeclMixin):
mlirlite.build_call(builder, dl_fn_name, dl_self, 2, dl_ret, 0)
return 0
def _emit_del_dtor(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, cur: mlirlite.Value*, var_ty: mlirlite.LLVMType*, struct_ty: mlirlite.LLVMType*) -> int:
"""Call `__del__` on a heap object before freeing it; null pointers are skipped."""
if pool is None or builder is None or mod is None or cur is None or struct_ty is None: return 0
cls_nm: str = self.Gen.StructGen.get_class_name_by_type(pool, struct_ty)
if cls_nm is None: return 0
if self.Gen.ExprGenOps._resolve_dunder_by_class(pool, mod, cls_nm, "__del__", trans, struct_ty) is None: return 0
nulp: mlirlite.Value* = mlirlite.ConstNull(pool, var_ty, "null")
if nulp is None: return 0
cond: mlirlite.Value* = mlirlite.build_icmp(builder, mlirlite.ICMP_NE, cur, nulp)
if cond is None: return 0
func: mlirlite.Function* = builder.Func
if func is None: return 0
cnt: int = trans._label_counter
trans._label_counter = cnt + 1
b1: str = pool.alloc(32)
b2: str = pool.alloc(32)
if b1 is None or b2 is None: return 0
viperlib.snprintf(b1, 32, "del.dtor.%d", cnt)
viperlib.snprintf(b2, 32, "del.dtor.end.%d", cnt)
dtor_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, b1)
end_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, b2)
if dtor_bb is None or end_bb is None: return 0
mlirlite.build_cond_br(builder, cond, dtor_bb, end_bb)
mlirlite.position_at_end(builder, dtor_bb)
self.Gen.ExprGenOps._try_named_dunder(pool, builder, mod, cur, "__del__", trans, None, 0)
mlirlite.build_br(builder, end_bb)
mlirlite.position_at_end(builder, end_bb)
return 1
# translate_assert - translate `assert cond[, msg]` into a single testcheck.check(...) call:
# a true condition is silent (ok_msg=None), a false one reports FAIL via the standard path
# (no printf/exit/exception; the checker prints nothing when the condition holds).