From 83d309509be106950bfc50b60d6b6210397cc24a Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Mon, 2 Nov 2026 10:29:05 +0800 Subject: [PATCH] feat(gen): tuple-literal simultaneous assignment (`a, b = b, a`) A tuple/list literal on the right of a multi-target assignment used to return no value (`assignment rhs is None ... [kind=51]`). It is now staged and assigned target by target: - every RHS element is evaluated left to right and parked in its own temporary slot, so a swap always reads the old values; - each target is then written by the ordinary single-target path, which keeps const checks, coercion, attribute/field stores, array and list element stores (`__setitem__`) and ownership handling unchanged; - the temporary's life tag is copied from the staged expression, so pointer assignments keep their existing frame/heap lifetime; - starred elements/targets and compiler-owned `new ... slife` objects fail fast (they cannot be staged without changing their semantics). Probe: ints, array elements (`a[i], a[i+1] = a[i+1], a[i]`) and struct fields (`p.x, p.y = p.y, p.x`) all swap correctly (exit 2304); self-host 110/110; PtrOpProbe, HashLibTest, GenericTest3 and ListTest still pass. --- App/lib/core/Gen/AssignGenMixin.vp | 93 ++++++++++++++++++++++++++++++ App/lib/core/Gen/Translator.vp | 3 + 2 files changed, 96 insertions(+) diff --git a/App/lib/core/Gen/AssignGenMixin.vp b/App/lib/core/Gen/AssignGenMixin.vp index a9b3dad..2c92052 100644 --- a/App/lib/core/Gen/AssignGenMixin.vp +++ b/App/lib/core/Gen/AssignGenMixin.vp @@ -3456,6 +3456,90 @@ novirtual class AssignGenMixin: return slot # translate_assign - handle an Assign statement; returns the number of new variables + # _tuple_tmp_name - fresh temporary name for tuple-literal staging. + def _tuple_tmp_name(self, pool: memhub.MemBuddy*, trans: HT.Translator*) -> str: + seq: int = trans.TupleTmpSeq + trans.TupleTmpSeq = seq + 1 + buf: str = pool.alloc(32) + if buf is None: return None + viperlib.snprintf(buf, 32, "__vpc_tup%d__", seq) + return buf + + # _translate_tuple_literal_assign - `a, b = e1, e2` with a tuple/list literal RHS. + # Every element is evaluated first and parked in its own slot, then each target is + # assigned by the ordinary single-target path (const checks, attributes, elements, + # __setitem__, ownership). This makes swaps (`a, b = b, a`) use the old values. + def _translate_tuple_literal_assign(self, trans: HT.Translator*, asgn: ast.Assign*, + targets: list[ast.AST*]*, pool: memhub.MemBuddy*, + builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*) -> int: + tup_t: ast.Tuple* = ast.Tuple*(targets.get(0)) + rhs_tup: ast.Tuple* = ast.Tuple*(asgn.value) + if tup_t is None or tup_t.elts is None or rhs_tup is None or rhs_tup.elts is None: + self.Gen.TypeGenErr.fatal_error(asgn, "tuple assignment: both sides must be tuple/list literals with elements", "SyntaxError") + return 0 + tgt_elts: list[ast.AST*]* = tup_t.elts + rhs_elts: list[ast.AST*]* = rhs_tup.elts + tn: sizet = len(tgt_elts) + rn: sizet = len(rhs_elts) + if tn != rn: + self.Gen.TypeGenErr.fatal_error(asgn, f"tuple assignment arity mismatch: {int(tn)} target(s) but {int(rn)} value(s)", "TypeError") + return 0 + if tn == 0: return 0 + GenApi.fail_if_fstring_escape(asgn.value, "tuple assignment") + # 1) Evaluate every value left-to-right into a private slot. + tmp_ids: list[str]* = list[str](pool, 8) + if tmp_ids is None: return 0 + for i in range(int(rn)): + rv: ast.AST* = rhs_elts.get(i) + if rv is None: + self.Gen.TypeGenErr.fatal_error(asgn, "tuple assignment: a right-hand side element is missing", "SyntaxError") + return 0 + if rv.kind() == ast.ASTKind.Starred: + self.Gen.TypeGenErr.fatal_error(rv, "starred tuple assignment values are not supported; write the elements explicitly", "SyntaxError") + return 0 + if rv.kind() == ast.ASTKind.New: + nn_t: ast.New* = ast.New*(rv) + if nn_t is not None and nn_t.Life == ast.NEW_LIFE_SLIFE: + self.Gen.TypeGenErr.fatal_error(rv, "tuple assignment cannot stage a compiler-owned `new ... slife` object; assign it on its own line", "LifetimeError") + return 0 + val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, rv, None, 0, trans) + if val is None or val.Ty is None: + self.Gen.TypeGenErr.fatal_error(rv, "tuple assignment: the right-hand side element could not be translated", "TypeError") + return 0 + tid: str = self._tuple_tmp_name(pool, trans) + if tid is None: return 0 + slot: mlirlite.Value* = self.Gen.VarGen.get_or_create_sym(trans.SymTab, pool, builder, tid, val.Ty) + if slot is None or slot.Ty is None: + self.Gen.TypeGenErr.fatal_error(rv, "tuple assignment: temporary slot allocation failed", "InternalCompilerError") + return 0 + if slot.Ty.Pointee != 0: + val = self.Gen.ExprGenBase.coerce_to_type(builder, val, slot.Ty.Pointee, rv, trans) + mlirlite.build_store(builder, val, slot) + # Life follows the staged value, not the temporary's default tag. + self.Gen.VarGen.set_var_life(trans.SymTab, tid, self.life_of_expr(trans, rv, None)) + tmp_ids.append(tid) + # 2) One ordinary assignment per target from its staged copy. + added: int = 0 + for i in range(int(tn)): + te: ast.AST* = tgt_elts.get(i) + if te is None: continue + if te.kind() == ast.ASTKind.Starred: + self.Gen.TypeGenErr.fatal_error(te, "starred tuple assignment targets are not supported; name each target", "SyntaxError") + return added + tname: ast.Name* = ast.Name(pool, tmp_ids.get(i), ast.ASTCtx.Load) + if tname is None: continue + tname.lineno = te.lineno + tname.col_offset = te.col_offset + one: list[ast.AST*]* = list[ast.AST*](pool, 8) + if one is None: return added + one.append(te) + syn: ast.Assign* = ast.Assign(pool, one, tname) + if syn is None: return added + syn.lineno = te.lineno + syn.col_offset = te.col_offset + added += self.translate_assign(trans, syn) + return added + def translate_assign(self, trans: HT.Translator*, node: ast.AST*) -> int: """Translate an Assign statement; return the number of new variables.""" @@ -3474,6 +3558,15 @@ novirtual class AssignGenMixin: builder: mlirlite.IRBuilder* = trans._cur_builder mod: mlirlite.LLVMModule* = trans.Module + # Simultaneous tuple assignment (`a, b = b, a`, `arr[i], arr[j] = ...`). + # A tuple/list literal RHS is not a struct value; evaluate every element first, + # park each in its own temporary, then run one ordinary assignment per target. + if len(targets) == 1: + tt0: ast.AST* = targets.get(0) + if tt0 is not None and (tt0.kind() == ast.ASTKind.Tuple or tt0.kind() == ast.ASTKind.List): + if asgn.value is not None and (asgn.value.kind() == ast.ASTKind.Tuple or asgn.value.kind() == ast.ASTKind.List): + return self._translate_tuple_literal_assign(trans, asgn, targets, pool, builder, mod) + # const target: fail fast before assignment (Name/Attribute/Subscript) for ti_c in range(int(len(targets))): tgt_c: ast.AST* = targets.get(ti_c) diff --git a/App/lib/core/Gen/Translator.vp b/App/lib/core/Gen/Translator.vp index 687b375..b703afc 100644 --- a/App/lib/core/Gen/Translator.vp +++ b/App/lib/core/Gen/Translator.vp @@ -211,6 +211,8 @@ novirtual class Translator: # Globally unique lambda ID counter (generates hoisted names __lambda_{id}). # Kept independent of _closure_env_count, which resets per function, so IDs stay unique. _lambda_count: int + # Tuple-literal assignment temporaries (`a, b = b, a`): unique name counter. + TupleTmpSeq: int # Set of nonlocal variables across the function chain (collected by pre-scan). # The outermost function allocates their heaps and stores addresses in @nlheap.{sha1}.{name}. _nl_chain_names: rawptr @@ -289,6 +291,7 @@ novirtual class Translator: self._closure_env_offsets = 0 self._closure_env_count = 0 self._lambda_count = 0 + self.TupleTmpSeq = 0 self._nl_chain_names = None self._nl_chain_count = 0 self._nl_chain_done = 0