cgen: store all eightbytes of a global/chained dot-field unwrap success (#16)

This commit is contained in:
2026-06-28 09:54:59 +09:00
parent 6b36b050d7
commit 3526725b5a
3 changed files with 216 additions and 0 deletions

View File

@@ -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

View File

@@ -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).

View File

@@ -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);
};