From 6f45fc016b6ba43af07580a2cb6e2ca6645b61d4 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Wed, 4 Nov 2026 18:04:36 +0800 Subject: [PATCH] feat(obs): move own values into fields and elements `A.n = B` / `arr[i] = B` now transfer ownership: the source shadow stops owning the object (no double free) and later uses of B fail as moved. The language server marks the source moved too. --- App/lib/core/Gen/AssignGenMixin.vp | 20 ++++++++++++++++++++ App/lib/lsp/LspChecks.vp | 9 +++++++++ 2 files changed, 29 insertions(+) diff --git a/App/lib/core/Gen/AssignGenMixin.vp b/App/lib/core/Gen/AssignGenMixin.vp index 4addd15..0a83584 100644 --- a/App/lib/core/Gen/AssignGenMixin.vp +++ b/App/lib/core/Gen/AssignGenMixin.vp @@ -1075,6 +1075,23 @@ novirtual class AssignGenMixin: return (ast.Name*)(uo_b.operand).id return None + def _move_out_assigned_source(self, trans: HT.Translator*, rhs: ast.AST*, + pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*) -> int: + """OBS move: after `field = ownVar` / `arr[i] = ownVar`, the source no longer owns the value.""" + if trans is None or rhs is None or trans.StrictOBS == 0: return 0 + if rhs.kind() != ast.ASTKind.Name: return 0 + src_nm: ast.Name* = ast.Name*(rhs) + if src_nm.id is None: return 0 + src_slot: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, src_nm.id) + if src_slot is None: return 0 + if self.Gen.VarGen._SlifeOwned(src_slot) == 0: return 0 + # Cancel the source's scope-exit release (the field/element now holds the pointer) and + # reject later uses of the moved binding. + ssh: mlirlite.Value* = self.Gen.VarGen._SlifeShadow(src_slot) + if ssh is not None: self.Gen.VarGen._SlifeNoteStore(pool, builder, ssh, 0) + self.Gen.VarGen._FreedMark(src_slot) + return 1 + 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 @@ -3838,6 +3855,7 @@ novirtual class AssignGenMixin: vol_sw: int = 0 if sub_wr is not None: vol_sw = self.Gen.ExprGenAttr.base_pointee_is_volatile(pool, trans, sub_wr.value) mlirlite.build_store(builder, store_val, elem_ptr, vol_sw) + self._move_out_assigned_source(trans, asgn.value, pool, builder) continue # get_subscript_ptr returned None: try the __setitem__ overload for custom classes sub_asgn: ast.Subscript* = ast.Subscript*(target) @@ -3920,6 +3938,7 @@ novirtual class AssignGenMixin: if elem_ptr_gen is not None and elem_ptr_gen.Ty is not None: if elem_ptr_gen.Ty.Pointee != 0: store_val_gen = self.Gen.ExprGenBase.coerce_to_type(builder, rhs_val, elem_ptr_gen.Ty.Pointee, node, trans) mlirlite.build_store(builder, store_val_gen, elem_ptr_gen) + self._move_out_assigned_source(trans, asgn.value, pool, builder) setitem_done = 1 if setitem_done == 0: self.Gen.TypeGenErr.fatal_error(target, "subscript ptr is None") continue @@ -4018,6 +4037,7 @@ novirtual class AssignGenMixin: if at_wr is not None: if self.Gen.ExprGenAttr.base_pointee_is_volatile(pool, trans, at_wr.value) != 0: vol_fw = 1 mlirlite.build_store(builder, store_val, field_ptr, vol_fw) + self._move_out_assigned_source(trans, asgn.value, pool, builder) continue # Guard: field_ptr is None -> build a detailed error message attr_node: ast.Attribute* = ast.Attribute*(target) diff --git a/App/lib/lsp/LspChecks.vp b/App/lib/lsp/LspChecks.vp index 8621ee3..b5c88a9 100644 --- a/App/lib/lsp/LspChecks.vp +++ b/App/lib/lsp/LspChecks.vp @@ -1645,6 +1645,15 @@ def _WalkChecks(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, tn_m: ast.Name* = ast.Name*(tm) if tn_m.id is not None and _OwnHas(tn_m.id) != 0: _MovedAdd(pool, sn_a.id) + # `obj.field = ownVar` / `arr[i] = ownVar`: the field/element takes ownership. + ti_m2: int = 0 + while ti_m2 < int(len(asn.targets)): + tm2: ast.AST* = asn.targets.get(sizet(ti_m2)) + ti_m2 += 1 + if tm2 is not None and (tm2.kind() == ast.ASTKind.Attribute + or tm2.kind() == ast.ASTKind.Subscript): + _MovedAdd(pool, sn_a.id) + break ti_c: int = 0 while ti_c < int(len(asn.targets)): tc: ast.AST* = asn.targets.get(sizet(ti_c))