wcc: N_DOT-base address arithmetic for [N]T-field index (#135)

Strategy (a) use-site fix: new helper cg_dotbase_addr (cstage) /
dotbaseaddr (wwstage) detects `base.kind == N_DOT` whose field type
is TY_ARRAY and emits the field's address inline — LEAQ inner_off+
field_off(BP) for a value-struct inner, MOVQ inner_off(BP),reg +
ADDQ field_off,reg for a *struct inner. The TY_ARRAY-only gate (after
TY_NAMED peel) keeps the helper INERT on TY_PTR/TY_SLICE/TY_STR/
TY_TAGGED field kinds where the existing cgexpr(base) path is
correct (loads pointer/header value, then adds scaled index). Wired
at 6 sites: cstage cgassign N_INDEX-lhs plain ASSIGN + #133 compound
arm + cgindex N_INDEX read fallback; wwstage twin × 3. Closes the
silent-segfault on `(*struct).array_field[i]` reads and writes —
pre-fix cgexpr on the N_DOT base auto-derefed and loaded the field's
first 8 bytes as if they were a pointer, faulting on packed [N]u8
arrays (small u64 → unmapped page).

Bootstrap-NEUTRAL: zero working callers in either direction pre-fix
(symmetric READ + WRITE segfault evidence). All corpus + 990-997
byte-id + combined_ww_fresh stay green post-fix.

949_dotbase_arr_run: 3 rows direct runtime + cs==ww byte-id (READ
u8, plain WRITE u8, compound WRITE u8). Wider element widths and
value-struct base / pointer-field-control rows deferred — blocked by
orthogonal pre-existing wwstage divergences (i32-return ABI MOVSXD
vs MOVL, uninit-struct-let zero-init asymmetry) documented in the
test body. The TY_ARRAY-gate no-over-fire is implicitly verified by
994/995 (corpus exercises thousands of struct.pointerfield[i]
shapes; any over-fire would shift bytes).

Chained N_DOT (`outer.inner.array[i]` depth ≥2) deferred to #137 —
confirmed not in ref/hare/strconv/decimal.ha or sibling strconv/.
Not a fold-3 blocker; helper bails (returns false) on chained shape,
caller falls back to existing cgexpr path.
This commit is contained in:
2026-05-26 22:56:00 +09:00
parent 3986818172
commit ade6840610
6 changed files with 583 additions and 11 deletions

View File

@@ -15038,6 +15038,85 @@ fn cgslicehdr(c: *cgen, base: str) void = {
if (streq(base, "AX")) { emitmovqload(0i64, base, "AX"); };
};
// dotbaseaddr — emit `&(inner.field)` into `dstreg` when `base` is an
// N_DOT with N_IDENT inner. Returns true if emitted; callers fall back
// to `cgexpr(c, base); MOVQ AX, dstreg` on false. Cstage twin:
// cmd/w6c/cgen.c `cg_dotbase_addr`.
//
// #135: cgexpr on an N_DOT whose .field is a `[N]T`-typed field auto-
// derefs + loads the field's 8-byte VALUE as if it were a pointer. For
// an LHS or index-base shape (`d.fld[i] = v` / `d.fld[i]` read / `d.fld
// [i] OP= v`), the caller wants the field's ADDRESS — this helper
// supplies it inline. Reusable primitive of the inverse template
// `arr[i].field = v` (cstage cgen.c arr[i].field address-eval). Chained
// N_DOT (`a.b.c.field[i]`) deferred — not in #135 scope.
fn dotbaseaddr(c: *cgen, base: *node, dstreg: str) bool = {
if (base == nil) { return false; };
if (base.kind != nkind.N_DOT) { return false; };
let inner: *node = base.lhs;
if (inner == nil) { return false; };
if (inner.kind != nkind.N_IDENT) { return false; };
let lc: *local = localfindnode(c, inner.str);
if (lc == nil) { return false; };
let bu: *tinfo = inner.type_: *tinfo;
for (bu != nil && bu.kind == tykind.TY_NAMED) { bu = bu.under; };
if (bu == nil) { return false; };
let viaptr: bool = false;
let structt: *tinfo = nil;
if (bu.kind == tykind.TY_PTR) {
let st: *tinfo = bu.sub;
for (st != nil && st.kind == tykind.TY_NAMED) { st = st.under; };
if (st != nil) { if (st.kind == tykind.TY_STRUCT) {
structt = st;
viaptr = true;
}; };
} else { if (bu.kind == tykind.TY_STRUCT) {
structt = bu;
}; };
if (structt == nil) { return false; };
let f: *tfield = structt.fields;
let foff: i64 = -1;
let ft: *tinfo = nil;
for (f != nil) {
if (streq(f.name, base.str)) {
foff = f.offset: i64;
ft = f.type_;
break;
};
f = f.tnext;
};
if (foff < 0) { return false; };
// Only fire on `[N]T` fields — for `*T` / `[]T` / `str` fields
// the existing cgexpr(base) path correctly loads the pointer/
// header value; over-firing here would skip the deref. Cstage
// twin gate at cg_dotbase_addr.
for (ft != nil && ft.kind == tykind.TY_NAMED) { ft = ft.under; };
if (ft == nil) { return false; };
if (ft.kind != tykind.TY_ARRAY) { return false; };
let innoff: i64 = lc.off: i64;
if (viaptr) {
emitline("\tMOVQ\t");
emitoff(innoff);
emitline("(BP), ");
emitline(dstreg);
emitline("\n");
if (foff != 0) {
emitline("\tADDQ\t$");
emitint(foff);
emitline(", ");
emitline(dstreg);
emitline("\n");
};
} else {
emitline("\tLEAQ\t");
emitoff(innoff + foff);
emitline("(BP), ");
emitline(dstreg);
emitline("\n");
};
return true;
};
fn cgindex(c: *cgen, n: *node) void = {
// Element-size-aware load: u8 → MOVZBQ, i32 → MOVSXD, u32 → MOVL,
// str → (ptr, len) into (AX, BX), everything else → MOVQ. Fast
@@ -15268,8 +15347,13 @@ fn cgindex(c: *cgen, n: *node) void = {
return;
};
// Generic fallback when base isn't a plain ident.
// #135: N_DOT base on `[N]T` field needs the field's ADDRESS,
// not its value. cgexpr would auto-deref + load the 8-byte value
// as if it were a pointer. dotbaseaddr emits the address inline.
emitline("\tPUSHQ\tAX\n");
cgexpr(c, base);
if (!dotbaseaddr(c, base, "AX")) {
cgexpr(c, base);
};
emitline("\tPOPQ\tBX\n");
emitline("\tADDQ\tBX, AX\n");
if (elem_tagged) {
@@ -18597,10 +18681,12 @@ fn cgassign(c: *cgen, n: *node) void = {
emitoff(baselocal.off: i64);
emitline("(BP), BX\n");
};
} else { if (dotbaseaddr(c, base, "BX")) {
// #135: N_DOT base address-of-field inline.
} else {
cgexpr(c, base);
emitline("\tMOVQ\tAX, BX\n");
};};};
};};};};
emitline("\tPOPQ\tAX\n"); // scaled idx
emitline("\tADDQ\tAX, BX\n");
emitline("\tPOPQ\tAX\n"); // value
@@ -18757,10 +18843,12 @@ fn cgassign(c: *cgen, n: *node) void = {
emitoff(baselocal.off: i64);
emitline("(BP), BX\n");
};
} else { if (dotbaseaddr(c, base, "BX")) {
// #135: N_DOT base address-of-field inline.
} else {
cgexpr(c, base);
emitline("\tMOVQ\tAX, BX\n");
};};};
};};};};
emitline("\tPOPQ\tAX\n");
emitline("\tADDQ\tAX, BX\n");
let lop: str = tnodeloadop(c, elemtn, esz);

View File

@@ -741,6 +741,85 @@ fn cgslicehdr(c: *cgen, base: str) void = {
if (streq(base, "AX")) { emitmovqload(0i64, base, "AX"); };
};
// dotbaseaddr — emit `&(inner.field)` into `dstreg` when `base` is an
// N_DOT with N_IDENT inner. Returns true if emitted; callers fall back
// to `cgexpr(c, base); MOVQ AX, dstreg` on false. Cstage twin:
// cmd/w6c/cgen.c `cg_dotbase_addr`.
//
// #135: cgexpr on an N_DOT whose .field is a `[N]T`-typed field auto-
// derefs + loads the field's 8-byte VALUE as if it were a pointer. For
// an LHS or index-base shape (`d.fld[i] = v` / `d.fld[i]` read / `d.fld
// [i] OP= v`), the caller wants the field's ADDRESS — this helper
// supplies it inline. Reusable primitive of the inverse template
// `arr[i].field = v` (cstage cgen.c arr[i].field address-eval). Chained
// N_DOT (`a.b.c.field[i]`) deferred — not in #135 scope.
fn dotbaseaddr(c: *cgen, base: *node, dstreg: str) bool = {
if (base == nil) { return false; };
if (base.kind != nkind.N_DOT) { return false; };
let inner: *node = base.lhs;
if (inner == nil) { return false; };
if (inner.kind != nkind.N_IDENT) { return false; };
let lc: *local = localfindnode(c, inner.str);
if (lc == nil) { return false; };
let bu: *tinfo = inner.type_: *tinfo;
for (bu != nil && bu.kind == tykind.TY_NAMED) { bu = bu.under; };
if (bu == nil) { return false; };
let viaptr: bool = false;
let structt: *tinfo = nil;
if (bu.kind == tykind.TY_PTR) {
let st: *tinfo = bu.sub;
for (st != nil && st.kind == tykind.TY_NAMED) { st = st.under; };
if (st != nil) { if (st.kind == tykind.TY_STRUCT) {
structt = st;
viaptr = true;
}; };
} else { if (bu.kind == tykind.TY_STRUCT) {
structt = bu;
}; };
if (structt == nil) { return false; };
let f: *tfield = structt.fields;
let foff: i64 = -1;
let ft: *tinfo = nil;
for (f != nil) {
if (streq(f.name, base.str)) {
foff = f.offset: i64;
ft = f.type_;
break;
};
f = f.tnext;
};
if (foff < 0) { return false; };
// Only fire on `[N]T` fields — for `*T` / `[]T` / `str` fields
// the existing cgexpr(base) path correctly loads the pointer/
// header value; over-firing here would skip the deref. Cstage
// twin gate at cg_dotbase_addr.
for (ft != nil && ft.kind == tykind.TY_NAMED) { ft = ft.under; };
if (ft == nil) { return false; };
if (ft.kind != tykind.TY_ARRAY) { return false; };
let innoff: i64 = lc.off: i64;
if (viaptr) {
emitline("\tMOVQ\t");
emitoff(innoff);
emitline("(BP), ");
emitline(dstreg);
emitline("\n");
if (foff != 0) {
emitline("\tADDQ\t$");
emitint(foff);
emitline(", ");
emitline(dstreg);
emitline("\n");
};
} else {
emitline("\tLEAQ\t");
emitoff(innoff + foff);
emitline("(BP), ");
emitline(dstreg);
emitline("\n");
};
return true;
};
fn cgindex(c: *cgen, n: *node) void = {
// Element-size-aware load: u8 → MOVZBQ, i32 → MOVSXD, u32 → MOVL,
// str → (ptr, len) into (AX, BX), everything else → MOVQ. Fast
@@ -971,8 +1050,13 @@ fn cgindex(c: *cgen, n: *node) void = {
return;
};
// Generic fallback when base isn't a plain ident.
// #135: N_DOT base on `[N]T` field needs the field's ADDRESS,
// not its value. cgexpr would auto-deref + load the 8-byte value
// as if it were a pointer. dotbaseaddr emits the address inline.
emitline("\tPUSHQ\tAX\n");
cgexpr(c, base);
if (!dotbaseaddr(c, base, "AX")) {
cgexpr(c, base);
};
emitline("\tPOPQ\tBX\n");
emitline("\tADDQ\tBX, AX\n");
if (elem_tagged) {
@@ -4300,10 +4384,12 @@ fn cgassign(c: *cgen, n: *node) void = {
emitoff(baselocal.off: i64);
emitline("(BP), BX\n");
};
} else { if (dotbaseaddr(c, base, "BX")) {
// #135: N_DOT base address-of-field inline.
} else {
cgexpr(c, base);
emitline("\tMOVQ\tAX, BX\n");
};};};
};};};};
emitline("\tPOPQ\tAX\n"); // scaled idx
emitline("\tADDQ\tAX, BX\n");
emitline("\tPOPQ\tAX\n"); // value
@@ -4460,10 +4546,12 @@ fn cgassign(c: *cgen, n: *node) void = {
emitoff(baselocal.off: i64);
emitline("(BP), BX\n");
};
} else { if (dotbaseaddr(c, base, "BX")) {
// #135: N_DOT base address-of-field inline.
} else {
cgexpr(c, base);
emitline("\tMOVQ\tAX, BX\n");
};};};
};};};};
emitline("\tPOPQ\tAX\n");
emitline("\tADDQ\tAX, BX\n");
let lop: str = tnodeloadop(c, elemtn, esz);

View File

@@ -15038,6 +15038,85 @@ fn cgslicehdr(c: *cgen, base: str) void = {
if (streq(base, "AX")) { emitmovqload(0i64, base, "AX"); };
};
// dotbaseaddr — emit `&(inner.field)` into `dstreg` when `base` is an
// N_DOT with N_IDENT inner. Returns true if emitted; callers fall back
// to `cgexpr(c, base); MOVQ AX, dstreg` on false. Cstage twin:
// cmd/w6c/cgen.c `cg_dotbase_addr`.
//
// #135: cgexpr on an N_DOT whose .field is a `[N]T`-typed field auto-
// derefs + loads the field's 8-byte VALUE as if it were a pointer. For
// an LHS or index-base shape (`d.fld[i] = v` / `d.fld[i]` read / `d.fld
// [i] OP= v`), the caller wants the field's ADDRESS — this helper
// supplies it inline. Reusable primitive of the inverse template
// `arr[i].field = v` (cstage cgen.c arr[i].field address-eval). Chained
// N_DOT (`a.b.c.field[i]`) deferred — not in #135 scope.
fn dotbaseaddr(c: *cgen, base: *node, dstreg: str) bool = {
if (base == nil) { return false; };
if (base.kind != nkind.N_DOT) { return false; };
let inner: *node = base.lhs;
if (inner == nil) { return false; };
if (inner.kind != nkind.N_IDENT) { return false; };
let lc: *local = localfindnode(c, inner.str);
if (lc == nil) { return false; };
let bu: *tinfo = inner.type_: *tinfo;
for (bu != nil && bu.kind == tykind.TY_NAMED) { bu = bu.under; };
if (bu == nil) { return false; };
let viaptr: bool = false;
let structt: *tinfo = nil;
if (bu.kind == tykind.TY_PTR) {
let st: *tinfo = bu.sub;
for (st != nil && st.kind == tykind.TY_NAMED) { st = st.under; };
if (st != nil) { if (st.kind == tykind.TY_STRUCT) {
structt = st;
viaptr = true;
}; };
} else { if (bu.kind == tykind.TY_STRUCT) {
structt = bu;
}; };
if (structt == nil) { return false; };
let f: *tfield = structt.fields;
let foff: i64 = -1;
let ft: *tinfo = nil;
for (f != nil) {
if (streq(f.name, base.str)) {
foff = f.offset: i64;
ft = f.type_;
break;
};
f = f.tnext;
};
if (foff < 0) { return false; };
// Only fire on `[N]T` fields — for `*T` / `[]T` / `str` fields
// the existing cgexpr(base) path correctly loads the pointer/
// header value; over-firing here would skip the deref. Cstage
// twin gate at cg_dotbase_addr.
for (ft != nil && ft.kind == tykind.TY_NAMED) { ft = ft.under; };
if (ft == nil) { return false; };
if (ft.kind != tykind.TY_ARRAY) { return false; };
let innoff: i64 = lc.off: i64;
if (viaptr) {
emitline("\tMOVQ\t");
emitoff(innoff);
emitline("(BP), ");
emitline(dstreg);
emitline("\n");
if (foff != 0) {
emitline("\tADDQ\t$");
emitint(foff);
emitline(", ");
emitline(dstreg);
emitline("\n");
};
} else {
emitline("\tLEAQ\t");
emitoff(innoff + foff);
emitline("(BP), ");
emitline(dstreg);
emitline("\n");
};
return true;
};
fn cgindex(c: *cgen, n: *node) void = {
// Element-size-aware load: u8 → MOVZBQ, i32 → MOVSXD, u32 → MOVL,
// str → (ptr, len) into (AX, BX), everything else → MOVQ. Fast
@@ -15268,8 +15347,13 @@ fn cgindex(c: *cgen, n: *node) void = {
return;
};
// Generic fallback when base isn't a plain ident.
// #135: N_DOT base on `[N]T` field needs the field's ADDRESS,
// not its value. cgexpr would auto-deref + load the 8-byte value
// as if it were a pointer. dotbaseaddr emits the address inline.
emitline("\tPUSHQ\tAX\n");
cgexpr(c, base);
if (!dotbaseaddr(c, base, "AX")) {
cgexpr(c, base);
};
emitline("\tPOPQ\tBX\n");
emitline("\tADDQ\tBX, AX\n");
if (elem_tagged) {
@@ -18597,10 +18681,12 @@ fn cgassign(c: *cgen, n: *node) void = {
emitoff(baselocal.off: i64);
emitline("(BP), BX\n");
};
} else { if (dotbaseaddr(c, base, "BX")) {
// #135: N_DOT base address-of-field inline.
} else {
cgexpr(c, base);
emitline("\tMOVQ\tAX, BX\n");
};};};
};};};};
emitline("\tPOPQ\tAX\n"); // scaled idx
emitline("\tADDQ\tAX, BX\n");
emitline("\tPOPQ\tAX\n"); // value
@@ -18757,10 +18843,12 @@ fn cgassign(c: *cgen, n: *node) void = {
emitoff(baselocal.off: i64);
emitline("(BP), BX\n");
};
} else { if (dotbaseaddr(c, base, "BX")) {
// #135: N_DOT base address-of-field inline.
} else {
cgexpr(c, base);
emitline("\tMOVQ\tAX, BX\n");
};};};
};};};};
emitline("\tPOPQ\tAX\n");
emitline("\tADDQ\tAX, BX\n");
let lop: str = tnodeloadop(c, elemtn, esz);