diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 9ba00b73..e5b628ed 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -5209,6 +5209,64 @@ cgexpr(Cg *c, Node *n, Local *locals) fatal("array-literal store at assignment " "unwired (task #32)"); } + /* #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 + * arm covered only a LOCAL / via-ptr base: it gated !is_global + * and its enclosing N_DOT arm requires an N_IDENT local base, + * so the global case fell to the generic single-word store + * (DROPPED w1/w2) and the chained case never reached any field + * arm (SILENT both-stage, byte-id blind). Route the destination + * ADDRESS through cgplaceaddr (which resolves a global LEAQ root + * and a chained deref+offset spine uniformly) and feed the SAME + * {AX,DX,CX} producer-shift materialise. cgplaceaddr clobbers + * AX/CX, so it runs BEFORE cgexpr(rhs) and the address is saved + * across the call. In-cap struct field only (size<=24, #12 + * scope); a float-bearing / over-cap success variant loud-stops + * at the producer. Local/via-ptr single-dot stays on #12. */ + if (n->op == TK_ASSIGN && n->lhs && n->lhs->kind == N_DOT + && n->lhs->lhs + && n->rhs && (n->rhs->kind == N_TRYUNW + || n->rhs->kind == N_TRYPROP)) { + Node *db = n->lhs->lhs; + int chained = (db->kind == N_DOT); + int globalbase = 0; + if (db->kind == N_IDENT + && localfind(locals, db->str) == 0 + && let_islet(db->str)) { + Type *dbu = type_chase_named(db->type); + if (dbu && dbu->kind == TY_STRUCT) + globalbase = 1; + } + Type *fu = type_chase_named(n->lhs->type); + if ((chained || globalbase) && fu + && fu->kind == TY_STRUCT + && (int)fu->size <= 24 + && (fu->size % 8 == 0 || fu->size % 8 == 1 + || fu->size % 8 == 2 || fu->size % 8 == 4)) { + int ssz = (int)fu->size; + if (!cgplaceaddr(c, n->lhs, D_BX, locals)) + fatal("#16: global/chained aggregate " + "unwrap field dest unresolved " + "(cgplaceaddr)"); + ins1(c, A_PUSHQ, areg(D_BX)); + cgexpr(c, n->rhs, locals); + ins1(c, A_POPQ, areg(D_BX)); + int regs[3] = { D_AX, D_DX, D_CX }; + int full = ssz / 8; + int tail = ssz % 8; + for (int i = 0; i < full; i++) + ins2(c, A_MOVQ, areg(regs[i]), + amem(D_BX, 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_BX, full * 8)); + } + break; + } + } /* p.x = v or p.x += v where p.x is a struct field * (direct or via *struct). For compound ops we read-modify- * write the field; for plain `=` we just write. The base diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 34a66a3f..da0ac5f2 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -8353,6 +8353,86 @@ fn cgcall(c: *cgen, n: *syntax.node) void = { fn cgassign(c: *cgen, n: *syntax.node) void = { let lhs: *syntax.node = n.lhs; + // #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 arm covered only a + // LOCAL / via-ptr base (its enclosing block requires localfindnode + // != nil), so the global case fell to the generic single-word store + // (DROPPED w1/w2) and the chained case never reached any field arm + // (SILENT both-stage, byte-id blind). Route the dest ADDRESS through + // cgplaceaddr (global LEAQ root + chained deref+offset spine) and + // feed the SAME {AX,DX,CX} producer-shift materialise. cgplaceaddr + // clobbers AX/CX, so it runs BEFORE cgexpr(rhs) and the address is + // saved across the call. In-cap struct field only (#12 scope); + // float/over-cap loud-stop at the producer. Local/via-ptr single-dot + // stays on the #12 arm. Mirrors cstage cgen.c N_ASSIGN #16 arm. + if (lhs != nil) { if (lhs.kind == syntax.nkind.N_DOT + && lhs.lhs != nil && n.op == syntax.tkind.TK_ASSIGN + && n.rhs != nil + && (n.rhs.kind == syntax.nkind.N_TRYUNW + || n.rhs.kind == syntax.nkind.N_TRYPROP)) { + let db: *syntax.node = lhs.lhs; + let chained: bool = db.kind == syntax.nkind.N_DOT; + let globalbase: bool = false; + if (db.kind == syntax.nkind.N_IDENT) { + if (localfindnode(c, db.str) == nil) { + if (isletvar(c, db.str)) { + let dbu: *syntax.tinfo = tichase(db.type_: *syntax.tinfo); + if (dbu != nil) { if (dbu.kind == syntax.tykind.TY_STRUCT) { + globalbase = true; + }; }; + }; + }; + }; + if (chained || globalbase) { + let fu: *syntax.tinfo = tichase(lhs.type_: *syntax.tinfo); + if (fu != nil) { if (fu.kind == syntax.tykind.TY_STRUCT) { + let ssz: i32 = fu.size: i32; + if (ssz <= 24) { + let tlm: i32 = ssz - (ssz / 8) * 8; + if (tlm == 0 || tlm == 1 || tlm == 2 || tlm == 4) { + if (!cgplaceaddr(c, lhs, "BX")) { + let m16: str = "#16: global/chained aggregate unwrap field dest unresolved (cgplaceaddr)\n"; + os.write(2, m16.ptr, m16.len: u64); + os.exit(1); + }; + emitline("\tPUSHQ\tBX\n"); + cgexpr(c, n.rhs); + emitline("\tPOPQ\tBX\n"); + let full: i32 = ssz / 8; + let i: i32 = 0; + for (i < full) { + let reg: str = "AX"; + if (i == 1) { reg = "DX"; }; + if (i == 2) { reg = "CX"; }; + emitline("\tMOVQ\t"); + emitline(reg); + emitline(", "); + emitdispreg((i * 8): i64, "BX"); + emitline("\n"); + i += 1; + }; + if (tlm > 0) { + let top: str = "MOVB"; + if (tlm == 4) { top = "MOVL"; }; + if (tlm == 2) { top = "MOVW"; }; + let treg: str = "AX"; + if (full == 1) { treg = "DX"; }; + if (full == 2) { treg = "CX"; }; + emitline("\t"); + emitline(top); + emitline("\t"); + emitline(treg); + emitline(", "); + emitdispreg((full * 8): i64, "BX"); + emitline("\n"); + }; + return; + }; + }; + }; }; + }; + }; }; // #145 (c1.5a): bulk slice-copy-assign `s.arr[lo:hi] = bs` (LHS is // N_SLICE). No legacy cgassign arm catches N_SLICE — the statement // silently emitted nothing (both stages, byte-id-green, #263-class). diff --git a/test/lang/global_chained_unwrap_test.ww b/test/lang/global_chained_unwrap_test.ww new file mode 100644 index 00000000..1d19dba5 --- /dev/null +++ b/test/lang/global_chained_unwrap_test.ww @@ -0,0 +1,78 @@ +// global_chained_unwrap_test — #16: a single-dot aggregate-field unwrap +// `g.f = mk()!` whose base is a module-GLOBAL value-struct, or `o.m.f = mk()!` +// whose base is a CHAINED struct field, dropped payload words (SILENT, both +// stages, byte-id-BLIND) — still broken after #12 fixed the LOCAL / via-ptr +// single-dot case. The #12 single-dot arm gated out the global base and its +// enclosing arm only admits an N_IDENT local base, so a global base fell to +// the generic single-word store (w1/w2 dropped) and a chained base never +// reached any field arm at all. The fix routes the destination ADDRESS +// through the cgplaceaddr spine (global LEAQ root + chained deref+offset) and +// feeds the SAME {AX=w0,DX=w1,CX=w2} producer-shift materialise the LOCAL arm +// uses. +// +// Both stages emit identically wrong asm pre-fix, so the byte-id gate is BLIND +// to the drop — these VALUE asserts are the sole tooth. POISON-SEED every dest +// member to 9 (the struct's init literal) so a dropped w1/w2 reads 9 != the +// expected value. Reverting either stage's #16 arm reverts the dropped word to +// 9 and reddens. +// +// Coverage = the global value-struct base (2-eightbyte w1 + 3-eightbyte w2 +// teeth) and the chained base both as a value-local root (`o.m.f`) and through +// a pointer root (`p.m.f` = (*p).m.f). The sub-8-tail-THROUGH-unwrap case is +// NOT pinnable here for the same reasons as struct_unwrap_test (array success +// variant checker-rejected; sub-8-tail struct success trips #15's frame-edge +// class, now closed but covered there). +// +// Inline @test fns, not a row-table: the cases vary in LHS PLACE shape +// (gb.f / gb24.f / o.m.f / p.m.f) and struct width, not in data values over +// one operation, so a row-array `[](in,exp){}` cannot express them — and that +// form is itself blocked by cgen #111. Mirrors struct_unwrap_test's #12 pins. + +package global_chained_unwrap_test; + +type e = !i32; +type s2 = struct { a: i64, b: i64 }; // 2 eightbytes: w0=a, w1=b +type s24 = struct { a: i64, b: i64, c: i64 }; // 3 eightbytes: w0,w1,w2 +type box = struct { f: s2, nb: i64 }; +type box24 = struct { f: s24, nb: i64 }; +type outer = struct { m: box, on: i64 }; + +fn mk2() (s2 | e) = { return s2 { a = 111i64, b = 222i64 }; }; +fn mk24() (s24 | e) = { return s24 { a = 111i64, b = 222i64, c = 333i64 }; }; + +let gb: box = box { f = s2{a=9i64,b=9i64}, nb = 7i64 }; +let gb24: box24 = box24 { f = s24{a=9i64,b=9i64,c=9i64}, nb = 7i64 }; + +@test fn global_dot() void = { // g.f = mk()! — module-global value base + gb.f = mk2()!; + assert(gb.f.a == 111i64); + assert(gb.f.b == 222i64); // w1 tooth + assert(gb.nb == 7i64); // neighbour field intact +}; + +@test fn global_dot_w2() void = { // 3-eightbyte global, w2 (R8->CX) tooth + gb24.f = mk24()!; + assert(gb24.f.a == 111i64); + assert(gb24.f.b == 222i64); + assert(gb24.f.c == 333i64); // w2 tooth + assert(gb24.nb == 7i64); +}; + +@test fn chained_dot() void = { // o.m.f = mk()! — chained, value-local root + let o: outer = outer { m = box { f = s2{a=9i64,b=9i64}, nb = 5i64 }, on = 3i64 }; + o.m.f = mk2()!; + assert(o.m.f.a == 111i64); + assert(o.m.f.b == 222i64); // w1 tooth + assert(o.m.nb == 5i64); // neighbour field intact + assert(o.on == 3i64); +}; + +@test fn chained_dot_via_ptr() void = { // p.m.f = mk()! — chained, pointer root + let o: outer = outer { m = box { f = s2{a=9i64,b=9i64}, nb = 5i64 }, on = 3i64 }; + let p: *outer = &o; + p.m.f = mk2()!; + assert(o.m.f.a == 111i64); + assert(o.m.f.b == 222i64); // w1 tooth + assert(o.m.nb == 5i64); + assert(o.on == 3i64); +};