cgen: route aggregate field-to-field assignment through the aggregate copier

The direct-field assignment arms enumerate CALL, STRUCTLIT, and local
IDENT producers; an addressable N_DOT/N_INDEX/deref rhs fell through to
the scalar tail, so a 16-byte struct field copied only its first word.
Resolve both places through the existing address funnels and use the
tail-aware aggregate copier. Both stages.
This commit is contained in:
2026-08-07 22:59:52 +09:00
parent c654e97db1
commit 078708770b
3 changed files with 148 additions and 0 deletions

View File

@@ -5257,6 +5257,39 @@ cgexpr(Cg *c, Node *n, Local *locals)
fatal("array-literal store at assignment " fatal("array-literal store at assignment "
"unwired (task #32)"); "unwired (task #32)");
} }
/* A plain aggregate field-to-field assignment is a memory copy,
* not a scalar expression/store. The legacy direct-field arms
* below enumerate CALL, STRUCTLIT, and local IDENT producers; an
* addressable N_DOT/N_INDEX/deref rhs fell through, so a 16-byte
* time.instant copied only its first word. Resolve both places
* through the existing generic address funnels and use the single
* tail-aware aggregate copier. Calls/literals/unwraps stay on their
* specialized ABI paths, and tagged/str/slice fields are excluded by
* the destination type gate. */
if (n->op == TK_ASSIGN && n->lhs
&& n->lhs->kind == N_DOT && n->lhs->lhs
&& (n->lhs->lhs->kind == N_IDENT
|| n->lhs->lhs->kind == N_DOT
|| (n->lhs->lhs->kind == N_UN
&& n->lhs->lhs->op == TK_STAR))
&& n->rhs) {
Type *au = type_chase_named(n->lhs->type);
int memrhs = n->rhs->kind == N_IDENT
|| n->rhs->kind == N_DOT
|| n->rhs->kind == N_INDEX
|| (n->rhs->kind == N_UN && n->rhs->op == TK_STAR);
if (au && (au->kind == TY_STRUCT || au->kind == TY_ARRAY
|| au->kind == TY_TUPLE) && memrhs) {
if (!cgplaceaddr(c, n->lhs, D_BX, locals))
fatal("aggregate field destination unresolved");
ins1(c, A_PUSHQ, areg(D_BX));
if (!aggarg_srcaddr(c, n->rhs, D_SI, locals))
fatal("aggregate field source unresolved");
ins1(c, A_POPQ, areg(D_BX));
cg_aggcopy(c, (int)au->size);
break;
}
}
/* #16: a single-dot aggregate-field unwrap store whose base /* #16: a single-dot aggregate-field unwrap store whose base
* is a module-GLOBAL value-struct (`g.f = mk()!`) or a * is a module-GLOBAL value-struct (`g.f = mk()!`) or a
* CHAINED struct field (`o.m.f = mk()!`). The #12 single-dot * CHAINED struct field (`o.m.f = mk()!`). The #12 single-dot

View File

@@ -8719,6 +8719,48 @@ fn cgassign(c: *cgen, n: *syntax.node) void = {
}; };
}; };
}; };
// A plain aggregate field-to-field assignment is a memory copy, not
// a scalar expression/store. The direct-field arms below enumerate
// CALL, STRUCTLIT and local IDENT producers; an addressable DOT/INDEX/
// deref rhs fell through, so a 16-byte time.instant copied one word.
// Resolve both places through the existing address funnels and use the
// canonical tail-aware aggregate copier. Calls/literals/unwraps and
// tagged/str/slice fields remain on their specialized ABI paths.
if (lhs != nil && n.rhs != nil && n.op == syntax.tkind.TK_ASSIGN) {
if (lhs.kind == syntax.nkind.N_DOT && lhs.lhs != nil
&& (lhs.lhs.kind == syntax.nkind.N_IDENT
|| lhs.lhs.kind == syntax.nkind.N_DOT
|| (lhs.lhs.kind == syntax.nkind.N_UN
&& lhs.lhs.op == syntax.tkind.TK_STAR))) {
let au: *syntax.tinfo = lhs.type_: *syntax.tinfo;
au = tichase(au);
let memrhs: bool = n.rhs.kind == syntax.nkind.N_IDENT
|| n.rhs.kind == syntax.nkind.N_DOT
|| n.rhs.kind == syntax.nkind.N_INDEX
|| (n.rhs.kind == syntax.nkind.N_UN
&& n.rhs.op == syntax.tkind.TK_STAR);
if (au != nil && memrhs) {
if (au.kind == syntax.tykind.TY_STRUCT
|| au.kind == syntax.tykind.TY_ARRAY
|| au.kind == syntax.tykind.TY_TUPLE) {
if (!cgplaceaddr(c, lhs, "BX")) {
let md: str = "aggregate field destination unresolved\n";
os.write(2, md.ptr, md.len: u64);
os.exit(1);
};
emitline("\tPUSHQ\tBX\n");
if (!aggargsrcaddr(c, n.rhs, "SI")) {
let ms: str = "aggregate field source unresolved\n";
os.write(2, ms.ptr, ms.len: u64);
os.exit(1);
};
emitline("\tPOPQ\tBX\n");
aggcopy(c, au.size: i32);
return;
};
};
};
};
// #21: a COMPOUND op on a whole tagged-union IDENT (`g OP= v` // #21: a COMPOUND op on a whole tagged-union IDENT (`g OP= v`
// with g:(int|bool)) is nonsense — the ident load-combine-store // with g:(int|bool)) is nonsense — the ident load-combine-store
// tail below reads and writes one word of the {payload,tag} box, // tail below reads and writes one word of the {payload,tag} box,

View File

@@ -0,0 +1,73 @@
// Addressable aggregate fields must copy every byte. A scalar fallback used
// to copy only the first word of a 16-byte nested value under both stages.
package aggregate_field_copy_test;
type pair = struct {
first: i64,
second: i64,
};
type outer = struct {
guard: i64,
value: pair,
tail: i64,
};
type nested = struct {
outer: outer,
};
let globalsource: outer;
let globaltarget: outer;
fn check(v: *outer, first: i64, second: i64) void = {
assert(v.value.first == first);
assert(v.value.second == second);
assert(v.guard == 91);
assert(v.tail == 92);
};
@test fn aggregate_field_memory_copy() void = {
let source: outer;
source.value.first = 11;
source.value.second = 12;
let target: outer;
target.guard = 91;
target.value.first = 1;
target.value.second = 2;
target.tail = 92;
target.value = source.value;
check(&target, 11, 12);
let pointer: *outer = ⌖
source.value.first = 21;
source.value.second = 22;
pointer.value = source.value;
check(&target, 21, 22);
let deep: nested;
deep.outer.value.first = 31;
deep.outer.value.second = 32;
target.value = deep.outer.value;
check(&target, 31, 32);
let indexed: [2]pair = [pair{first=5,second=6},
pair{first=35,second=36}];
target.value = indexed[1];
check(&target, 35, 36);
let pointed: pair = pair{first=37,second=38};
let sourcepointer: *pair = &pointed;
target.value = *sourcepointer;
check(&target, 37, 38);
globalsource.value.first = 41;
globalsource.value.second = 42;
globaltarget.guard = 91;
globaltarget.value.first = 3;
globaltarget.value.second = 4;
globaltarget.tail = 92;
globaltarget.value = globalsource.value;
check(&globaltarget, 41, 42);
};