diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 80b07ba9..1228092a 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -6731,6 +6731,107 @@ cgexpr(Cg *c, Node *n, Local *locals) cg_sret_dest_off = 0; break; } + /* C2c / #31-G: an IN-CAP aggregate-returning CALL into an + * indexed element `a[i] = mk()`. The #234 arm above only + * fires for an OVER-cap (sret/memory) return; the #270-1b + * arm below excludes N_CALL (it copies from a source + * ADDRESS, which a call result has none). An in-cap (≤24B) + * struct/array/tuple return leaves AX/DX/CX per the #4 + * cgreturn ABI but fell through to the 1-word scalar store + * (AX only) — dropping DX/CX (a silent field-drop, the + * documented-but-unfixed #31-G). Materialise the return into + * a frame scratch (the AX/DX/CX receive shape at the N_LET + * call-rhs site, cgen.c:3434), THEN compute &a[i] and word- + * copy scratch → dest. Scratch-first (not a dest spill across + * the call) keeps the call at the frame's natural alignment. + * In-cap only (cg_sret_retsize==0). */ + if ((is_arr || is_sl || is_ptr) && n->op == TK_ASSIGN + && esubu && (esubu->kind == TY_STRUCT + || esubu->kind == TY_ARRAY + || esubu->kind == TY_TUPLE) + && esz > 8 + && n->rhs->kind == N_CALL + && cg_sret_retsize(esub) == 0) { + int scr = cg_tagscr_slot(c, &locals, esz); + cgexpr(c, n->rhs, locals); /* call → AX/DX/CX */ + /* AX/DX/CX → scratch (mirror cgen.c:3434 receive). */ + int regs[3] = { D_AX, D_DX, D_CX }; + int full = esz / 8; + int tail = esz % 8; + for (int i = 0; i < full; i++) + ins2(c, A_MOVQ, areg(regs[i]), + amem(D_BP, scr + i * 8)); + if (tail > 0) { + int op = (tail == 4) ? A_MOVL + : (tail == 2) ? A_MOVW : A_MOVB; + ins2(c, op, areg(regs[full]), + amem(D_BP, scr + full * 8)); + } + /* dest &a[i] → BX (mirror #121 / #270-1b resolve) */ + cgexpr(c, n->lhs->rhs, locals); /* idx → AX */ + if (esz > 1) { + ins2(c, A_MOVQ, aimm(esz), areg(D_CX)); + ins2(c, A_IMULQ, areg(D_CX), areg(D_AX)); + } + ins1(c, A_PUSHQ, areg(D_AX)); + if (base->kind == N_IDENT) { + int off = localfind(locals, base->str); + int isglobal = (off == 0) + && let_islet(base->str); + if (isglobal && is_arr) + ins2(c, A_LEAQ, + masym(c, base->str), + areg(D_BX)); + else if (isglobal) + ins2(c, A_MOVQ, + masym(c, base->str), + areg(D_BX)); + else 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)); + } else if (cg_dotbase_addr(c, base, D_BX, locals)) { + /* N_DOT array-field base resolved inline. */ + } else { + cgexpr(c, base, locals); + ins2(c, A_MOVQ, areg(D_AX), areg(D_BX)); + } + ins1(c, A_POPQ, areg(D_AX)); + ins2(c, A_ADDQ, areg(D_AX), areg(D_BX)); + /* word-copy scratch → dest (tail-aware, mirror + * #270-1b copy). */ + int k = 0; + for (; k + 8 <= esz; k += 8) { + ins2(c, A_MOVQ, amem(D_BP, scr + k), + areg(D_AX)); + ins2(c, A_MOVQ, areg(D_AX), + amem(D_BX, k)); + } + if (k + 4 <= esz) { + ins2(c, A_MOVL, amem(D_BP, scr + k), + areg(D_AX)); + ins2(c, A_MOVL, areg(D_AX), + amem(D_BX, k)); + k += 4; + } + if (k + 2 <= esz) { + ins2(c, A_MOVW, amem(D_BP, scr + k), + areg(D_AX)); + ins2(c, A_MOVW, areg(D_AX), + amem(D_BX, k)); + k += 2; + } + if (k + 1 <= esz) { + ins2(c, A_MOVB, amem(D_BP, scr + k), + areg(D_AX)); + ins2(c, A_MOVB, areg(D_AX), + amem(D_BX, k)); + k += 1; + } + break; + } /* #121 (write-face of leg-b): a tuple-LITERAL rhs into an * indexed element `a[i] = (3,4)`. A literal has no source * ADDRESS, so the ident/dot/deref copy arm below can't reach diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 62634d90..3b4ad792 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -9123,6 +9123,137 @@ fn cgassign(c: *cgen, n: *syntax.node) void = { c.sretdestoff = 0; return; }; + // C2c / #31-G: an IN-CAP aggregate-returning CALL into + // an indexed element `a[i] = mk()`. The #234 arm above + // only fires for an OVER-cap (sret) return; the #270-1b + // arm below copies from a source ADDRESS (which a call + // result has none). An in-cap (<=24B) struct/array/tuple + // return leaves AX/DX/CX per the #4 cgreturn ABI but fell + // to the 1-word scalar store (AX only) — dropping DX/CX + // (the documented-but-unfixed #31-G). Materialise the + // return into a frame scratch (the AX/DX/CX receive shape, + // cstage cgen.c:3434), THEN compute &a[i] and word-copy + // scratch -> dest. Scratch-first (not a dest spill across + // the call) keeps the call at the frame's natural + // alignment. esz>8 non-str/non-slice IS struct/array/tuple + // here (tagged returned above). In-cap only + // (callsretsize==0). Mirror of cstage cgen.c C2c arm. + if (n.rhs.kind == syntax.nkind.N_CALL && esz > 8 + && !isstrtype(c, elemtn) && !isslicetype(c, elemtn) + && callsretsize(c, n.rhs) == 0) { + let scrc2: i32 = tagscradd(c, esz); + cgexpr(c, n.rhs); // call -> AX/DX/CX + // AX/DX/CX -> scratch (mirror cstage cgen.c:3434). + let full: i32 = esz / 8; + let tail: i32 = esz % 8; + let wi: i32 = 0; + for (wi < full) { + let rn: str = "AX"; + if (wi == 1) { rn = "DX"; } + else { if (wi == 2) { rn = "CX"; }; }; + emitline("\tMOVQ\t"); + emitline(rn); + emitline(", "); + emitoff((scrc2 + wi * 8): i64); + emitline("(BP)\n"); + wi += 1; + }; + if (tail > 0) { + let top: str = "MOVB"; + if (tail == 4) { top = "MOVL"; } + else { if (tail == 2) { top = "MOVW"; }; }; + let treg: str = "AX"; + if (full == 1) { treg = "DX"; } + else { if (full == 2) { treg = "CX"; }; }; + emitline("\t"); + emitline(top); + emitline("\t"); + emitline(treg); + emitline(", "); + emitoff((scrc2 + full * 8): i64); + emitline("(BP)\n"); + }; + // dest &a[i] -> BX (#121 / #270-1b base resolve) + cgexpr(c, idx); + if (esz > 1) { + emitline("\tMOVQ\t$"); + emitint(esz: i64); + emitline(", CX\n"); + emitline("\tIMULQ\tCX, AX\n"); + }; + emitline("\tPUSHQ\tAX\n"); + if (isglobalarr) { + emitline("\tLEAQ\t"); + emitsymname(c, globalname); + emitline("(SB), BX\n"); + } else { if (isglobalptr) { + emitline("\tMOVQ\t"); + emitsymname(c, globalname); + emitline("(SB), BX\n"); + } else { if (baselocal != nil) { + let tn2: *syntax.node = baselocal.tnode; + let isarr2: bool = false; + if (tn2 != nil) { if (tn2.kind == syntax.nkind.N_TARRAY) { isarr2 = true; }; }; + if (basealias) { + let bu60: *syntax.tinfo = tichase(base.type_: *syntax.tinfo); + if (bu60 != nil) { isarr2 = bu60.kind == syntax.tykind.TY_ARRAY; }; + }; + if (isarr2) { + emitline("\tLEAQ\t"); + emitoff(baselocal.off: i64); + emitline("(BP), BX\n"); + } else { + emitline("\tMOVQ\t"); + emitoff(baselocal.off: i64); + emitline("(BP), BX\n"); + }; + } else { if (dotbaseaddr(c, base, "BX")) { + } else { + cgexpr(c, base); + emitline("\tMOVQ\tAX, BX\n"); + };};};}; + emitline("\tPOPQ\tAX\n"); + emitline("\tADDQ\tAX, BX\n"); + // word-copy scratch -> dest (tail-aware, #270-1b copy) + let kc: i32 = 0; + for (kc + 8 <= esz) { + emitline("\tMOVQ\t"); + emitoff((scrc2 + kc): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tAX, "); + emitoff(kc: i64); + emitline("(BX)\n"); + kc += 8; + }; + if (kc + 4 <= esz) { + emitline("\tMOVL\t"); + emitoff((scrc2 + kc): i64); + emitline("(BP), AX\n"); + emitline("\tMOVL\tAX, "); + emitoff(kc: i64); + emitline("(BX)\n"); + kc += 4; + }; + if (kc + 2 <= esz) { + emitline("\tMOVW\t"); + emitoff((scrc2 + kc): i64); + emitline("(BP), AX\n"); + emitline("\tMOVW\tAX, "); + emitoff(kc: i64); + emitline("(BX)\n"); + kc += 2; + }; + if (kc + 1 <= esz) { + emitline("\tMOVB\t"); + emitoff((scrc2 + kc): i64); + emitline("(BP), AX\n"); + emitline("\tMOVB\tAX, "); + emitoff(kc: i64); + emitline("(BX)\n"); + kc += 1; + }; + return; + }; // #121 (write-face of leg-b): a tuple-LITERAL rhs into // an indexed element `a[i] = (3,4)`. A literal has no // source ADDRESS, so the ident/dot/deref copy arm below diff --git a/test/lang/idx_aggret_recv_test.ww b/test/lang/idx_aggret_recv_test.ww new file mode 100644 index 00000000..52567f65 --- /dev/null +++ b/test/lang/idx_aggret_recv_test.ww @@ -0,0 +1,112 @@ +// idx_aggret_recv_test — in-cap aggregate-returning CALL received into an +// INDEXED element `a[i] = mk()` (C2c / #31-G). Pre-fix the N_ASSIGN-into- +// N_INDEX path had no arm for an in-cap (<=24B, AX/DX/CX-return) struct / +// array / tuple call rhs: the #270-1b element-store arm gates its source to +// N_IDENT / N_DOT / STAR (a call result has no source address) and the #234 +// arm only fires for an OVER-cap (sret) return, so the call fell to the +// 1-word scalar store — only AX (member 0) was written, DX/CX dropped, AND +// the index-scale clobbered CX. Both stages emitted byte-IDENTICAL wrong asm +// (gate-blind, the #263 both-wrong form). Each @test asserts EVERY member (a +// dropped word fails); covers 2- and 3-eightbyte structs/tuples, an array-of- +// array element, const + runtime index, and local + GLOBAL + N_DOT-field + +// slice base. T2 keeps the cs==ww net. + +package idx_aggret_recv_test; + +type t2 = struct { a: i64, b: i64 }; +type t3 = struct { a: i64, b: i64, c: i64 }; +type holder = struct { arr: [3]t3 }; + +let g3: [2]t3 = [t3{a=0i64,b=0i64,c=0i64}, t3{a=0i64,b=0i64,c=0i64}]; + +fn mk2(x: i64) t2 = { return t2{a=x, b=x+10i64}; }; +fn mk3(x: i64) t3 = { return t3{a=x, b=x+1i64, c=x+2i64}; }; +fn mktup2() (i64, i64) = { return (3i64, 4i64); }; +fn mktup3() (i64, i64, i64) = { return (5i64, 6i64, 7i64); }; +fn mkarr() [3]i64 = { return [8i64, 9i64, 10i64]; }; +fn mkarr32() [3]i32 = { return [11i32, 22i32, 33i32]; }; +fn idx1() i64 = { return 1i64; }; + +@test fn struct_2eb() void = { + let a: [2]t2; + a[1] = mk2(5i64); + assert(a[1].a == 5i64); + assert(a[1].b == 15i64); +}; + +@test fn struct_3eb() void = { + let a: [2]t3; + a[1] = mk3(5i64); + assert(a[1].a == 5i64); + assert(a[1].b == 6i64); + assert(a[1].c == 7i64); +}; + +@test fn runtime_index() void = { + let a: [2]t3; + a[idx1()] = mk3(7i64); + assert(a[1].a == 7i64); + assert(a[1].b == 8i64); + assert(a[1].c == 9i64); +}; + +@test fn global_dest() void = { + g3[1] = mk3(9i64); + assert(g3[1].a == 9i64); + assert(g3[1].b == 10i64); + assert(g3[1].c == 11i64); +}; + +@test fn dot_base() void = { + let h: holder; + h.arr[2] = mk3(3i64); + assert(h.arr[2].a == 3i64); + assert(h.arr[2].b == 4i64); + assert(h.arr[2].c == 5i64); +}; + +@test fn slice_base() void = { + let buf: [2]t3; + let sl: []t3; + sl.ptr = buf.ptr: *t3; + sl.len = 2; + sl.cap = 2; + sl[1] = mk3(4i64); + assert(buf[1].a == 4i64); + assert(buf[1].b == 5i64); + assert(buf[1].c == 6i64); +}; + +@test fn tuple_2eb() void = { + let a: [2](i64, i64); + a[1] = mktup2(); + assert(a[1].0 == 3i64); + assert(a[1].1 == 4i64); +}; + +@test fn tuple_3eb() void = { + let a: [2](i64, i64, i64); + a[1] = mktup3(); + assert(a[1].0 == 5i64); + assert(a[1].1 == 6i64); + assert(a[1].2 == 7i64); +}; + +@test fn array_elem() void = { + let a: [2][3]i64; + a[1] = mkarr(); + assert(a[1][0] == 8i64); + assert(a[1][1] == 9i64); + assert(a[1][2] == 10i64); +}; + +// [3]i32 element = 12B: esz%8 != 0, so the receive-store and the word-copy +// both hit the sub-8 TAIL arm (MOVL of the DX low-half), which the +// 8-multiple rows above never exercise. +@test fn array_elem_tail() void = { + let a: [2][3]i32; + a[1] = mkarr32(); + assert(a[1][0] == 11i32); + assert(a[1][1] == 22i32); + assert(a[1][2] == 33i32); +};