From 078708770b910dd8f37d7351aabaa5a3e6589379 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 7 Aug 2026 22:59:52 +0900 Subject: [PATCH] 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. --- cmd/w6c/cgen.c | 33 ++++++++++++ selfhost/cmd/wcc/cgenexpr.ww | 42 +++++++++++++++ test/lang/aggregate_field_copy_test.ww | 73 ++++++++++++++++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 test/lang/aggregate_field_copy_test.ww diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index db22bb22..10a6e1b4 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -5257,6 +5257,39 @@ cgexpr(Cg *c, Node *n, Local *locals) fatal("array-literal store at assignment " "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 * is a module-GLOBAL value-struct (`g.f = mk()!`) or a * CHAINED struct field (`o.m.f = mk()!`). The #12 single-dot diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index a6b48325..19943fd7 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -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` // with g:(int|bool)) is nonsense — the ident load-combine-store // tail below reads and writes one word of the {payload,tag} box, diff --git a/test/lang/aggregate_field_copy_test.ww b/test/lang/aggregate_field_copy_test.ww new file mode 100644 index 00000000..4371cbb2 --- /dev/null +++ b/test/lang/aggregate_field_copy_test.ww @@ -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); +};