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.
This commit is contained in:
TermiNexus committed 2026-11-02 10:29:05 +08:00
1 parent 94d8c0d151
commit 83d309509b
2 files changed
+96

No files matched your search

+93
View File
@@ -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)
+3
View File
@@ -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