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:
1 parent
d9511a54ea
commit
5f981d7878
5 files changed
+85
-17
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in new issue
Block a user