From 3719ff1c6482f43493151526ad3b13a15ecd17dd Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sat, 27 Jun 2026 19:40:04 +0900 Subject: [PATCH] cgen: copy all eightbytes when a non-call aggregate assigns into a field of an indexed element (#11b) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The arr[i].f=src legacy assign block enumerated scalar field-type arms then fell to a 1-word scalar default, so a non-call aggregate source (ident/dot/index) cgexpr'd only its first word into AX and stored one eightbyte — silent on BOTH stages (byte-id blind). The non-indexed bases (local/deref/chained/global) reach the general assign resolver's canonical aggargsrcaddr+aggcopy; the indexed arm short-circuited before it. Route the indexed base through the block's own proven &arr[i] spine into the same aggargsrcaddr+aggcopy emitters (DRY — no third copy), dual-site symmetric. Unlike #11's in-cap arm, the source is a memory address so aggcopy is a pure memcpy: float bits and the sub-8 tail transport verbatim, no loud-stop needed. Did not fall through to the general resolver because its cgplaceaddr N_INDEX arm rejects a *[N]S (TY_PTR) base (latent resolver gap, filed separately). Contained to the indexed base + non-call aggregate-field rhs; value-asserting pins redden under each stage's independent revert. --- cmd/w6c/cgen.c | 51 +++++++ selfhost/cmd/wcc/cgenexpr.ww | 72 ++++++++++ .../idx_dot_src_aggregate_runonly_test.ww | 43 ++++++ test/lang/idx_dot_src_aggregate_test.ww | 136 ++++++++++++++++++ 4 files changed, 302 insertions(+) create mode 100644 test/lang/idx_dot_src_aggregate_runonly_test.ww create mode 100644 test/lang/idx_dot_src_aggregate_test.ww diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 5460a4e1..2cc0bb4e 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -5982,6 +5982,57 @@ cgexpr(Cg *c, Node *n, Local *locals) * wholly in AX; the scalar default's * single store is the correct receive. */ } + /* #11b: a NON-call AGGREGATE source into an + * aggregate field of an indexed element + * `arr[i].f = src` (src an ident / .g / index). + * The scalar default below loads only the + * source's FIRST word into AX and stores ONE + * word — dropping the rest (a SILENT both-stage + * member drop, the non-call twin of the #11 + * in-cap CALL arm above; byte-id blind). Unlike + * #11's GP AX/DX/CX cursor the source is a + * MEMORY address, so the shared mem-to-mem + * cg_aggcopy transports EVERY byte: a sub-8 + * tail and float bits copy verbatim, so NO + * tail/float/over-cap loud-stop is needed here + * (those #11 stops were register-cursor + * artefacts). Reuse the block's own &arr[i] + * spine (proven for [N]S / *[N]S / []S by the + * sibling arms) -> BX + foff, then funnel + * through aggarg_srcaddr (src -> SI) + + * cg_aggcopy — the ONE copy emitter the non- + * indexed bases use (DRY). fsz natural + * (ft->size). Mirrors wwstage cgenexpr.ww. */ + if (n->op == TK_ASSIGN && n->rhs + && n->rhs->kind != N_CALL && fu + && (fu->kind == TY_STRUCT || fu->kind == TY_ARRAY + || fu->kind == TY_TUPLE) && fsz > 8) { + cgexpr(c, idx, locals); + if (esz > 1) { + ins2(c, A_MOVQ, aimm(esz), areg(D_CX)); + ins2(c, A_IMULQ, areg(D_CX), areg(D_AX)); + } + if (is_arr) + ins2(c, A_LEAQ, amem(D_BP, off), areg(D_BX)); + else + ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX)); + ins2(c, A_ADDQ, areg(D_AX), areg(D_BX)); + if (viaptr) + ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_BX)); + if (foff != 0) + ins2(c, A_ADDQ, aimm(foff), areg(D_BX)); + /* spill dest across the source-address + * resolution (the #270-1b order: + * aggarg_srcaddr clobbers BX). */ + ins1(c, A_PUSHQ, areg(D_BX)); + if (!aggarg_srcaddr(c, n->rhs, D_SI, locals)) + fatal("#11b: aggregate field receive " + "arr[i].f=src - source shape unwired " + "(rule-7)"); + ins1(c, A_POPQ, areg(D_BX)); + cg_aggcopy(c, fsz); + break; + } if (n->op == TK_ASSIGN) { cgexpr(c, n->rhs, locals); ins1(c, A_PUSHQ, diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 28256854..6d0b48c2 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -10163,6 +10163,78 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { // in AX; the scalar default's MOVQ/MOVL AX // store is the correct 1-word receive. }; + // #11b: a NON-call AGGREGATE source into an aggregate + // field of an indexed element `arr[i].f = src` (src an + // ident / .g / index). The scalar default below loads + // only the source's FIRST word into AX and stores ONE + // word — dropping the rest (a SILENT both-stage member + // drop, the non-call twin of the #11 in-cap CALL arm + // above; byte-id blind). Unlike #11's GP AX/DX/CX cursor + // the source is a MEMORY address, so the shared mem-to- + // mem aggcopy transports EVERY byte: a sub-8 tail (MOVL/ + // MOVW/MOVB) and float bits copy verbatim, so NO tail/ + // float/over-cap loud-stop is needed here (those #11 + // stops were register-cursor artefacts). Reuse the + // block's own &arr[i] spine (proven for [N]S / *[N]S / + // []S by the sibling arms) -> BX + fi.foff, then funnel + // through aggargsrcaddr (src -> SI) + aggcopy — the ONE + // copy emitter the non-indexed bases use (DRY, rule 8). + // tsz natural (fi.fsz, type table). Mirrors cstage cgen.c. + if (n.op == syntax.tkind.TK_ASSIGN + && n.rhs != nil + && n.rhs.kind != syntax.nkind.N_CALL) { + let fk11b: *syntax.tinfo = tichase(fi.tnode.type_: *syntax.tinfo); + let isagg11b: bool = false; + if (fk11b != nil) { + if (fk11b.kind == syntax.tykind.TY_STRUCT + || fk11b.kind == syntax.tykind.TY_ARRAY + || fk11b.kind == syntax.tykind.TY_TUPLE) { + isagg11b = true; + }; + }; + if (isagg11b) { + let tsz11b: i32 = fi.fsz; + if (tsz11b > 8) { + // &arr[i].f -> BX (verbatim sibling-arm spine) + cgexpr(c, idx); + if (esz > 1) { + emitline("\tMOVQ\t$"); + emitint(esz: i64); + emitline(", CX\n"); + emitline("\tIMULQ\tCX, AX\n"); + }; + if (baseisarray) { + emitline("\tLEAQ\t"); + emitoff(lc.off: i64); + emitline("(BP), BX\n"); + } else { + emitline("\tMOVQ\t"); + emitoff(lc.off: i64); + emitline("(BP), BX\n"); + }; + emitline("\tADDQ\tAX, BX\n"); + if (viaptr) { emitline("\tMOVQ\t(BX), BX\n"); }; + if (fi.foff != 0) { + emitline("\tADDQ\t$"); + emitint(fi.foff: i64); + emitline(", BX\n"); + }; + // spill dest across the source-address resolution + // (the #270-1b order: aggargsrcaddr clobbers BX). + emitline("\tPUSHQ\tBX\n"); + if (!aggargsrcaddr(c, n.rhs, "SI")) { + let m11b: str = "#11b: aggregate field receive arr[i].f=src - source shape unwired (rule-7)\n"; + os.write(2, m11b.ptr, m11b.len: u64); + os.exit(1); + }; + emitline("\tPOPQ\tBX\n"); + aggcopy(c, tsz11b); + return; + }; + // tsz<=8 aggregate: one word, the scalar default's + // single store is the correct copy. + }; + }; // scalar plain `=` cgexpr(c, n.rhs); emitline("\tPUSHQ\tAX\n"); diff --git a/test/lang/idx_dot_src_aggregate_runonly_test.ww b/test/lang/idx_dot_src_aggregate_runonly_test.ww new file mode 100644 index 00000000..c2d07554 --- /dev/null +++ b/test/lang/idx_dot_src_aggregate_runonly_test.ww @@ -0,0 +1,43 @@ +// idx_dot_src_aggregate_runonly_test — #11b sub-8-tail anti-clobber through a +// LOCAL [N] array. The fix's MOVL (not MOVQ) tail +// is VALUE-verified here: the dropped tail would read the poison 9, and a tail +// WIDENED to MOVQ would write bytes 8..15 and smash the adjacent g at +12 — both +// caught by asserting f.c AND g. Exit-correct under BOTH stages. +// +// _runonly (excluded from the test-lang-byteid T2 gate) because a local +// [N] trips a SEPARATE PRE-EXISTING let-array slotsize cs!=ww +// FRAME divergence — ww sizes the array element by slotsize (s12 slot-padded), +// cstage by natural size — that PREDATES and is INDEPENDENT of #11b: empirically +// the #11b receive INSTRUCTIONS are byte-identical (both stages emit `MOVQ +// (SI),AX` then the `MOVL 8(SI),AX` tail); only the container's frame SIZE and +// the BP-relative offsets shift ($48 vs $64). The byte-id twin +// (idx_dot_src_aggregate_test) covers the same sub-8-tail shape via global- +// backed bases, where the slotsize bug does not bite (task #9). + +package idx_dot_src_aggregate_runonly_test; + +type t12 = struct { a: i32, b: i32, c: i32 }; +type s12 = struct { f: t12, g: i32 }; + +fn one() i64 = { return 1i64; }; + +@test fn subtail_local_ident_const() void = { + let a: [2]s12 = [s12{f=t12{a=9i32,b=9i32,c=9i32},g=9i32}, s12{f=t12{a=9i32,b=9i32,c=9i32},g=9i32}]; + let src: t12 = t12{a=10i32, b=20i32, c=30i32}; + a[1].f = src; + assert(a[1].f.a == 10i32); + assert(a[1].f.b == 20i32); + assert(a[1].f.c == 30i32); + assert(a[1].g == 9i32); + assert(a[0].f.a == 9i32); +}; + +@test fn subtail_local_ident_runtime() void = { + let a: [2]s12 = [s12{f=t12{a=9i32,b=9i32,c=9i32},g=9i32}, s12{f=t12{a=9i32,b=9i32,c=9i32},g=9i32}]; + let src: t12 = t12{a=10i32, b=20i32, c=30i32}; + a[one()].f = src; + assert(a[1].f.a == 10i32); + assert(a[1].f.b == 20i32); + assert(a[1].f.c == 30i32); + assert(a[1].g == 9i32); +}; diff --git a/test/lang/idx_dot_src_aggregate_test.ww b/test/lang/idx_dot_src_aggregate_test.ww new file mode 100644 index 00000000..7f56bbb3 --- /dev/null +++ b/test/lang/idx_dot_src_aggregate_test.ww @@ -0,0 +1,136 @@ +// idx_dot_src_aggregate_test — #11b: a NON-call AGGREGATE source assigned into +// an AGGREGATE field of an INDEXED element `arr[i].f = src` (src an ident or a +// field `.g`, NOT a function call). Pre-fix the N_DOT(N_INDEX) assign arm had no +// aggregate-source case: it fell to the 1-word scalar default — cgexpr loaded +// only the source's FIRST word into AX and stored ONE word, dropping the rest. +// BOTH stages emitted byte-IDENTICAL wrong asm (gate-blind, the #263 both-wrong +// form) — the non-call twin of #11 (idx_dot_aggret_recv_test, the in-cap CALL +// source). Unlike #11's GP AX/DX/CX cursor, the source here is a MEMORY address, +// so the shared mem-to-mem aggcopy transports EVERY byte — a sub-8 tail (MOVL/ +// MOVW/MOVB) and float bits copy verbatim, so the fix needs NO tail/float/over- +// cap loud-stop (those #11 stops were register-cursor artefacts). Each @test +// POISON-seeds the field + its neighbour with sentinel 9 (distinct from 0 AND +// every expected value) and asserts EVERY member: a dropped word reads 9 and +// fails. Covers IDENT and DOT sources, full=2/full=3 (CX word), the sub-8-tail +// MOVL row (anti-clobber: the 12B field's tail MUST stay MOVL — a widened MOVQ +// would smash the adjacent g at +12), foff!=0, a float-bearing field, and bases +// [N]S / *[N]S / []S with const + runtime index. The sub-8-tail rows use a +// GLOBAL-backed base: a LOCAL [N] array trips a SEPARATE +// PRE-EXISTING let-array slotsize cs!=ww FRAME divergence (see the _runonly +// twin; task #9), independent of #11b. T2 keeps the cs==ww net. + +package idx_dot_src_aggregate_test; + +type t16 = struct { a: i64, b: i64 }; +type t24 = struct { a: i64, b: i64, c: i64 }; +type t12 = struct { a: i32, b: i32, c: i32 }; +type ft = struct { x: f64, y: i64 }; +type s16 = struct { f: t16, g: i64 }; +type s24 = struct { f: t24, g: i64 }; +type sfnf = struct { g: i64, f: t16 }; +type sf = struct { f: ft, g: i64 }; +type s12 = struct { f: t12, g: i32 }; +type w16 = struct { pad: i64, inner: t16 }; +type w12 = struct { pad: i64, inner: t12 }; + +let g12p: [2]s12 = [s12{f=t12{a=9i32,b=9i32,c=9i32},g=9i32}, s12{f=t12{a=9i32,b=9i32,c=9i32},g=9i32}]; +let g12s: [2]s12 = [s12{f=t12{a=9i32,b=9i32,c=9i32},g=9i32}, s12{f=t12{a=9i32,b=9i32,c=9i32},g=9i32}]; +let g12d: [2]s12 = [s12{f=t12{a=9i32,b=9i32,c=9i32},g=9i32}, s12{f=t12{a=9i32,b=9i32,c=9i32},g=9i32}]; + +// full=2, [N]S value-array local, IDENT src, const index, foff=0 +@test fn struct16_local_ident_const() void = { + let a: [2]s16 = [s16{f=t16{a=9i64,b=9i64},g=9i64}, s16{f=t16{a=9i64,b=9i64},g=9i64}]; + let src: t16 = t16{a=40i64, b=20i64}; + a[1].f = src; + assert(a[1].f.a == 40i64); + assert(a[1].f.b == 20i64); + assert(a[1].g == 9i64); + assert(a[0].f.a == 9i64); +}; + +// full=2, [N]S local, DOT src (`w.inner`), const index, foff=0 +@test fn struct16_local_dot_const() void = { + let a: [2]s16 = [s16{f=t16{a=9i64,b=9i64},g=9i64}, s16{f=t16{a=9i64,b=9i64},g=9i64}]; + let w: w16 = w16{pad=7i64, inner=t16{a=40i64, b=20i64}}; + a[1].f = w.inner; + assert(a[1].f.a == 40i64); + assert(a[1].f.b == 20i64); + assert(a[1].g == 9i64); +}; + +// full=3 (CX word), [N]S local, IDENT src, runtime index, foff=0 +@test fn struct24_local_ident_runtime() void = { + let a: [2]s24 = [s24{f=t24{a=9i64,b=9i64,c=9i64},g=9i64}, s24{f=t24{a=9i64,b=9i64,c=9i64},g=9i64}]; + let src: t24 = t24{a=100i64, b=20i64, c=3i64}; + a[one()].f = src; + assert(a[1].f.a == 100i64); + assert(a[1].f.b == 20i64); + assert(a[1].f.c == 3i64); + assert(a[1].g == 9i64); +}; + +// foff != 0 (f at offset 8), [N]S local, IDENT src, const index +@test fn fnotfirst_local_ident() void = { + let a: [2]sfnf = [sfnf{g=9i64,f=t16{a=9i64,b=9i64}}, sfnf{g=9i64,f=t16{a=9i64,b=9i64}}]; + let src: t16 = t16{a=40i64, b=20i64}; + a[1].f = src; + assert(a[1].f.a == 40i64); + assert(a[1].f.b == 20i64); + assert(a[1].g == 9i64); +}; + +// float-bearing field (mem-to-mem copies float bits verbatim — no loud-stop), +// [N]S local, IDENT src, const index +@test fn float_local_ident() void = { + let a: [2]sf = [sf{f=ft{x=9.0f64,y=9i64},g=9i64}, sf{f=ft{x=9.0f64,y=9i64},g=9i64}]; + let src: ft = ft{x=1.5f64, y=7i64}; + a[1].f = src; + assert(a[1].f.x == 1.5f64); + assert(a[1].f.y == 7i64); + assert(a[1].g == 9i64); +}; + +// sub-8-tail (12B, MOVL tail), IDENT src, *[N]S -> global, const index +@test fn subtail_ptr_ident_const() void = { + let p: *[2]s12 = &g12p; + let src: t12 = t12{a=10i32, b=20i32, c=30i32}; + p[1].f = src; + assert(g12p[1].f.a == 10i32); + assert(g12p[1].f.b == 20i32); + assert(g12p[1].f.c == 30i32); + assert(g12p[1].g == 9i32); +}; + +// sub-8-tail (12B, MOVL tail), IDENT src, []S -> global, runtime index +@test fn subtail_slice_ident_runtime() void = { + let sl: []s12 = g12s[0:2]; + let src: t12 = t12{a=10i32, b=20i32, c=30i32}; + sl[one()].f = src; + assert(g12s[1].f.a == 10i32); + assert(g12s[1].f.b == 20i32); + assert(g12s[1].f.c == 30i32); + assert(g12s[1].g == 9i32); +}; + +// INDEX src (`srcarr[0]`), [N]S local, const index, foff=0 +@test fn struct16_local_index_src() void = { + let a: [2]s16 = [s16{f=t16{a=9i64,b=9i64},g=9i64}, s16{f=t16{a=9i64,b=9i64},g=9i64}]; + let srcarr: [2]t16 = [t16{a=40i64, b=20i64}, t16{a=99i64, b=99i64}]; + a[1].f = srcarr[0]; + assert(a[1].f.a == 40i64); + assert(a[1].f.b == 20i64); + assert(a[1].g == 9i64); +}; + +// sub-8-tail (12B, MOVL tail), DOT src (`w.inner`), *[N]S -> global, const index +@test fn subtail_ptr_dot_const() void = { + let p: *[2]s12 = &g12d; + let w: w12 = w12{pad=7i64, inner=t12{a=10i32, b=20i32, c=30i32}}; + p[1].f = w.inner; + assert(g12d[1].f.a == 10i32); + assert(g12d[1].f.b == 20i32); + assert(g12d[1].f.c == 30i32); + assert(g12d[1].g == 9i32); +}; + +fn one() i64 = { return 1i64; };