From 9ef9bef340242ea211e263289eaa13784367a0cc Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Wed, 13 May 2026 23:45:35 +0900 Subject: [PATCH] w6c+selfhost: cgen N_DOT N_INDEX-lhs branch (closes #8) cstage cmd/w6c/cgen.c gained the missing N_DOT N_INDEX-lhs branch. Covers both [N]*Struct and [N]Struct via fldloadop. wwstage already handled [N]*Struct since 7c75dd2; refactored to mirror cstage exactly and added [N]Struct. The spill workaround in dotchainresolve stays (Pike rule); task #14 retires it as a follow-up. Wwstage cgassign N_DOT(N_INDEX,...) silent store-drop discovered in scope, filed as task #16. --- Makefile | 8 +- cmd/w6c/cgen.c | 92 +++++++++++ selfhost/cmd/w6c/main.combined.ww | 136 ++++++++++------ selfhost/cmd/wcc/cgenexpr.ww | 123 +++++++++----- selfhost/cmd/wcc/cgenutil.ww | 13 +- selfhost/cmd/wwdump/main.combined.ww | 136 ++++++++++------ test/wcc/680_arr_elem_field.c | 232 +++++++++++++++++++++++++++ 7 files changed, 592 insertions(+), 148 deletions(-) create mode 100644 test/wcc/680_arr_elem_field.c diff --git a/Makefile b/Makefile index 833c3804..2baec585 100644 --- a/Makefile +++ b/Makefile @@ -217,7 +217,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_e2e $(BIN)/test_ffi $(BIN)/test_dyn $(BIN)/test_stdlib \ $(BIN)/test_at_test $(BIN)/test_let_global \ $(BIN)/test_int_cast_signed $(BIN)/test_dot_chain \ - $(BIN)/test_amp_dot \ + $(BIN)/test_amp_dot $(BIN)/test_arr_elem_field \ $(BIN)/test_field_signed $(BIN)/test_frame_argcount \ $(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \ $(BIN)/test_w6c_ww $(BIN)/test_ww_ww $(BIN)/test_self_rebuild \ @@ -297,6 +297,12 @@ $(BIN)/test_amp_dot: test/wcc/690_amp_dot.c $(BIN)/ww \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_arr_elem_field: test/wcc/680_arr_elem_field.c $(BIN)/ww \ + $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_field_signed: test/wcc/660_field_signed.c $(BIN)/ww \ $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index f325d280..d688d184 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -4253,6 +4253,98 @@ cgexpr(Cg *c, Node *n, Local *locals) } } } + /* `arr[i].field` — element-then-field through a `[N]*S` / + * `[N]S` (and slice/`*[N]S`) base. One branch covers both + * shapes: compute `&arr[i]` into BX, then either deref + * (`*Struct` element) or move-to-AX (value `Struct` element), + * so the leaf load is `(field.offset)(AX)` either way. + * Bypasses cgindex deliberately — cgindex's final MOVQ + * would truncate a value-struct element to 8 bytes. Mirrors + * selfhost/cmd/wcc/cgenexpr.ww's cgdot N_INDEX-lhs branch. */ + if (n->lhs && n->lhs->kind == N_INDEX && n->lhs->lhs + && n->lhs->lhs->kind == N_IDENT) { + Node *idxbase = n->lhs->lhs; + Type *elemt = n->lhs->type; + Type *elemu = (elemt && elemt->kind == TY_NAMED) + ? elemt->under : elemt; + Type *struct_t = NULL; + int viaptr = 0; + if (elemu && elemu->kind == TY_PTR) { + Type *inner = elemu->sub; + if (inner && inner->kind == TY_NAMED) + inner = inner->under; + if (inner && inner->kind == TY_STRUCT) { + struct_t = inner; + viaptr = 1; + } + } else if (elemu && elemu->kind == TY_STRUCT) { + struct_t = elemu; + } + if (struct_t) { + Tfield *f = NULL; + for (Tfield *fl = struct_t->fields; fl; fl = fl->next) + if (strcmp(fl->name, n->str) == 0) + { f = fl; break; } + Type *bt = idxbase->type; + Type *bu = (bt && bt->kind == TY_NAMED) + ? bt->under : bt; + int is_arr = bu && bu->kind == TY_ARRAY; + int is_sl = bu && bu->kind == TY_SLICE; + int is_ptr = bu && bu->kind == TY_PTR; + int off = localfind(locals, idxbase->str); + if (f != NULL && (is_arr || is_sl || is_ptr) + && off != 0) { + int esz = (int)elemt->size; + cgexpr(c, n->lhs->rhs, 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_AX)); + else + ins2(c, A_MOVQ, areg(D_BX), + areg(D_AX)); + int foff = (int)f->offset; + Type *ft = f->type; + Type *fu = (ft && ft->kind == TY_NAMED) + ? ft->under : ft; + if (fu && fu->kind == TY_STR) { + ins2(c, A_MOVQ, + amem(D_AX, foff + 8), + areg(D_BX)); + ins2(c, A_MOVQ, + amem(D_AX, foff + 0), + areg(D_AX)); + goto dot_done; + } + int g_isf32 = 0; + if (fld_isfloat(ft, &g_isf32)) { + int mov = g_isf32 + ? A_MOVSS : A_MOVSD; + ins2(c, mov, + amem(D_AX, foff), + areg(D_X0)); + goto dot_done; + } + int fsz = (int)(ft ? ft->size : 8); + int op = fldloadop(ft, fsz); + ins2(c, op, amem(D_AX, foff), + areg(D_AX)); + goto dot_done; + } + } + } /* Nested module-qualified field where the chain didn't fold to * a known shape (typical when w6c runs on a single file with * `use mod;` but no driver concatenation — the body's enum / diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 26bfb479..2f07acdd 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -7984,11 +7984,14 @@ export fn dotchainresolve(c: *cgen, n: *node, out: *dotchain) bool = { for (i >= 0) { let csi: *structinfo = structlookup(c, curstruct); if (csi == nil) { return false; }; - // Materialise the *node first; wwstage's cgen mis-emits the - // chained shape `stk[i].str` directly (task #8 — loses BX - // between the index load and the field deref), so always - // spill to an intermediate local before reading the str - // field. The cstage emits the same pattern for byte-identity. + // Spill `stk[i]` to a *node local before reading its `.str`. + // Task #8 closed the cgen bug behind this — cstage's N_DOT + // had no N_INDEX-lhs branch and fell through, returning the + // (AX, BX) cgindex shape as if it were the field. The fix + // lives in cmd/w6c/cgen.c N_DOT and the mirror in this + // module's cgenexpr.ww cgdot. The spill stays for now (Pike + // rule — workaround retirement is task #14), so cstage + // compiling this source emits byte-identical asm to wwstage. stepnd = stk[i]; if (stepnd == nil) { return false; }; stepnm = stepnd.str; @@ -9501,13 +9504,15 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; }; - // `xs[i].field` — slice/array/ptr-of-struct element field access. - // Without this the cgen falls through to the module-qualified - // SB fallback below and emits `MOVQ (SB), AX` (linker - // reports `undefined reference to `). cgexpr(c, lhs) - // dispatches to cgindex which leaves the element value in AX - // — for a []*T element that's the *T pointer, so we just chain - // the field load through (AX). + // `arr[i].field` — element-then-field through a `[N]*S` / `[N]S` + // (and slice/`*[N]S`) base. Without this the cgen falls through + // to the module-qualified SB fallback below and emits + // `MOVQ (SB), AX` (linker: `undefined reference to `). + // One branch covers both shapes: compute `&arr[i]` into BX, then + // either deref (`*Struct` element) or move-to-AX (value `Struct` + // element), so the leaf load is `(field.offset)(AX)` either way. + // Bypasses cgindex deliberately — cgindex's final MOVQ would + // truncate a value-struct element to 8 bytes. if (lhs != nil) { if (lhs.kind == nkind.N_INDEX) { let idxbase: *node = lhs.lhs; @@ -9516,52 +9521,85 @@ fn cgdot(c: *cgen, n: *node) void = { if (lc != nil) { if (lc.tnode != nil) { let tn: *node = lc.tnode; let elemt: *node = nil; + let baseisarray: bool = false; let tk: nkind = tn.kind; if (tk == nkind.N_TSLICE) { elemt = tn.lhs; }; - if (tk == nkind.N_TARRAY) { elemt = tn.lhs; }; + if (tk == nkind.N_TARRAY) { elemt = tn.lhs; baseisarray = true; }; if (tk == nkind.N_TPTR) { elemt = tn.lhs; }; - if (elemt != nil) { if (elemt.kind == nkind.N_TPTR) { - let inner: *node = elemt.lhs; - if (inner != nil) { if (inner.kind == nkind.N_TNAME) { - let sname: str = inner.str; - let si: *structinfo = structlookup(c, sname); - if (si != nil) { - let fi: *fieldinfo = si.fields; - for (fi != nil) { - if (streq(fi.fname, fld)) { - cgexpr(c, lhs); // AX = *Struct - if (isstrtype(c, fi.tnode)) { - emitline("\tMOVQ\t"); - emitdispreg((fi.foff + 8): i64, "AX"); - emitline(", BX\n"); - emitline("\tMOVQ\t"); - emitdispreg(fi.foff: i64, "AX"); - emitline(", AX\n"); - return; - }; - if (isfloattype(c, fi.tnode)) { - let mov: str = "MOVSD"; - if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; - emitline("\t"); - emitline(mov); - emitline("\t"); - emitdispreg(fi.foff: i64, "AX"); - emitline(", X0\n"); - return; - }; - let lop: str = fieldloadop(c, fi); - emitline("\t"); - emitline(lop); - emitline("\t"); + let sname: str; + sname.ptr = nil; sname.len = 0; + let viaptr: bool = false; + if (elemt != nil) { + if (elemt.kind == nkind.N_TPTR) { + let inner: *node = elemt.lhs; + if (inner != nil) { if (inner.kind == nkind.N_TNAME) { + sname = inner.str; + viaptr = true; + };}; + } else { if (elemt.kind == nkind.N_TNAME) { + sname = elemt.str; + };}; + }; + if (sname.len > 0) { + let si: *structinfo = structlookup(c, sname); + if (si != nil) { + let fi: *fieldinfo = si.fields; + for (fi != nil) { + if (streq(fi.fname, fld)) { + let esz: i32 = elemsizeofc(c, tn); + cgexpr(c, lhs.rhs); // idx → AX + 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), AX\n"); + } else { + emitline("\tMOVQ\tBX, AX\n"); + }; + if (isstrtype(c, fi.tnode)) { + emitline("\tMOVQ\t"); + emitdispreg((fi.foff + 8): i64, "AX"); + emitline(", BX\n"); + emitline("\tMOVQ\t"); emitdispreg(fi.foff: i64, "AX"); emitline(", AX\n"); return; }; - fi = fi.finext; + if (isfloattype(c, fi.tnode)) { + let mov: str = "MOVSD"; + if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t"); + emitdispreg(fi.foff: i64, "AX"); + emitline(", X0\n"); + return; + }; + let lop: str = fieldloadop(c, fi); + emitline("\t"); + emitline(lop); + emitline("\t"); + emitdispreg(fi.foff: i64, "AX"); + emitline(", AX\n"); + return; }; + fi = fi.finext; }; - };}; - };}; + }; + }; };}; };}; }; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index bcf7a8a3..a48fb172 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -1451,13 +1451,15 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; }; - // `xs[i].field` — slice/array/ptr-of-struct element field access. - // Without this the cgen falls through to the module-qualified - // SB fallback below and emits `MOVQ (SB), AX` (linker - // reports `undefined reference to `). cgexpr(c, lhs) - // dispatches to cgindex which leaves the element value in AX - // — for a []*T element that's the *T pointer, so we just chain - // the field load through (AX). + // `arr[i].field` — element-then-field through a `[N]*S` / `[N]S` + // (and slice/`*[N]S`) base. Without this the cgen falls through + // to the module-qualified SB fallback below and emits + // `MOVQ (SB), AX` (linker: `undefined reference to `). + // One branch covers both shapes: compute `&arr[i]` into BX, then + // either deref (`*Struct` element) or move-to-AX (value `Struct` + // element), so the leaf load is `(field.offset)(AX)` either way. + // Bypasses cgindex deliberately — cgindex's final MOVQ would + // truncate a value-struct element to 8 bytes. if (lhs != nil) { if (lhs.kind == nkind.N_INDEX) { let idxbase: *node = lhs.lhs; @@ -1466,52 +1468,85 @@ fn cgdot(c: *cgen, n: *node) void = { if (lc != nil) { if (lc.tnode != nil) { let tn: *node = lc.tnode; let elemt: *node = nil; + let baseisarray: bool = false; let tk: nkind = tn.kind; if (tk == nkind.N_TSLICE) { elemt = tn.lhs; }; - if (tk == nkind.N_TARRAY) { elemt = tn.lhs; }; + if (tk == nkind.N_TARRAY) { elemt = tn.lhs; baseisarray = true; }; if (tk == nkind.N_TPTR) { elemt = tn.lhs; }; - if (elemt != nil) { if (elemt.kind == nkind.N_TPTR) { - let inner: *node = elemt.lhs; - if (inner != nil) { if (inner.kind == nkind.N_TNAME) { - let sname: str = inner.str; - let si: *structinfo = structlookup(c, sname); - if (si != nil) { - let fi: *fieldinfo = si.fields; - for (fi != nil) { - if (streq(fi.fname, fld)) { - cgexpr(c, lhs); // AX = *Struct - if (isstrtype(c, fi.tnode)) { - emitline("\tMOVQ\t"); - emitdispreg((fi.foff + 8): i64, "AX"); - emitline(", BX\n"); - emitline("\tMOVQ\t"); - emitdispreg(fi.foff: i64, "AX"); - emitline(", AX\n"); - return; - }; - if (isfloattype(c, fi.tnode)) { - let mov: str = "MOVSD"; - if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; - emitline("\t"); - emitline(mov); - emitline("\t"); - emitdispreg(fi.foff: i64, "AX"); - emitline(", X0\n"); - return; - }; - let lop: str = fieldloadop(c, fi); - emitline("\t"); - emitline(lop); - emitline("\t"); + let sname: str; + sname.ptr = nil; sname.len = 0; + let viaptr: bool = false; + if (elemt != nil) { + if (elemt.kind == nkind.N_TPTR) { + let inner: *node = elemt.lhs; + if (inner != nil) { if (inner.kind == nkind.N_TNAME) { + sname = inner.str; + viaptr = true; + };}; + } else { if (elemt.kind == nkind.N_TNAME) { + sname = elemt.str; + };}; + }; + if (sname.len > 0) { + let si: *structinfo = structlookup(c, sname); + if (si != nil) { + let fi: *fieldinfo = si.fields; + for (fi != nil) { + if (streq(fi.fname, fld)) { + let esz: i32 = elemsizeofc(c, tn); + cgexpr(c, lhs.rhs); // idx → AX + 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), AX\n"); + } else { + emitline("\tMOVQ\tBX, AX\n"); + }; + if (isstrtype(c, fi.tnode)) { + emitline("\tMOVQ\t"); + emitdispreg((fi.foff + 8): i64, "AX"); + emitline(", BX\n"); + emitline("\tMOVQ\t"); emitdispreg(fi.foff: i64, "AX"); emitline(", AX\n"); return; }; - fi = fi.finext; + if (isfloattype(c, fi.tnode)) { + let mov: str = "MOVSD"; + if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t"); + emitdispreg(fi.foff: i64, "AX"); + emitline(", X0\n"); + return; + }; + let lop: str = fieldloadop(c, fi); + emitline("\t"); + emitline(lop); + emitline("\t"); + emitdispreg(fi.foff: i64, "AX"); + emitline(", AX\n"); + return; }; + fi = fi.finext; }; - };}; - };}; + }; + }; };}; };}; }; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 06deb6a0..dabd508f 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -2414,11 +2414,14 @@ export fn dotchainresolve(c: *cgen, n: *node, out: *dotchain) bool = { for (i >= 0) { let csi: *structinfo = structlookup(c, curstruct); if (csi == nil) { return false; }; - // Materialise the *node first; wwstage's cgen mis-emits the - // chained shape `stk[i].str` directly (task #8 — loses BX - // between the index load and the field deref), so always - // spill to an intermediate local before reading the str - // field. The cstage emits the same pattern for byte-identity. + // Spill `stk[i]` to a *node local before reading its `.str`. + // Task #8 closed the cgen bug behind this — cstage's N_DOT + // had no N_INDEX-lhs branch and fell through, returning the + // (AX, BX) cgindex shape as if it were the field. The fix + // lives in cmd/w6c/cgen.c N_DOT and the mirror in this + // module's cgenexpr.ww cgdot. The spill stays for now (Pike + // rule — workaround retirement is task #14), so cstage + // compiling this source emits byte-identical asm to wwstage. stepnd = stk[i]; if (stepnd == nil) { return false; }; stepnm = stepnd.str; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index ae904c3f..4af8faef 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -7984,11 +7984,14 @@ export fn dotchainresolve(c: *cgen, n: *node, out: *dotchain) bool = { for (i >= 0) { let csi: *structinfo = structlookup(c, curstruct); if (csi == nil) { return false; }; - // Materialise the *node first; wwstage's cgen mis-emits the - // chained shape `stk[i].str` directly (task #8 — loses BX - // between the index load and the field deref), so always - // spill to an intermediate local before reading the str - // field. The cstage emits the same pattern for byte-identity. + // Spill `stk[i]` to a *node local before reading its `.str`. + // Task #8 closed the cgen bug behind this — cstage's N_DOT + // had no N_INDEX-lhs branch and fell through, returning the + // (AX, BX) cgindex shape as if it were the field. The fix + // lives in cmd/w6c/cgen.c N_DOT and the mirror in this + // module's cgenexpr.ww cgdot. The spill stays for now (Pike + // rule — workaround retirement is task #14), so cstage + // compiling this source emits byte-identical asm to wwstage. stepnd = stk[i]; if (stepnd == nil) { return false; }; stepnm = stepnd.str; @@ -9501,13 +9504,15 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; }; - // `xs[i].field` — slice/array/ptr-of-struct element field access. - // Without this the cgen falls through to the module-qualified - // SB fallback below and emits `MOVQ (SB), AX` (linker - // reports `undefined reference to `). cgexpr(c, lhs) - // dispatches to cgindex which leaves the element value in AX - // — for a []*T element that's the *T pointer, so we just chain - // the field load through (AX). + // `arr[i].field` — element-then-field through a `[N]*S` / `[N]S` + // (and slice/`*[N]S`) base. Without this the cgen falls through + // to the module-qualified SB fallback below and emits + // `MOVQ (SB), AX` (linker: `undefined reference to `). + // One branch covers both shapes: compute `&arr[i]` into BX, then + // either deref (`*Struct` element) or move-to-AX (value `Struct` + // element), so the leaf load is `(field.offset)(AX)` either way. + // Bypasses cgindex deliberately — cgindex's final MOVQ would + // truncate a value-struct element to 8 bytes. if (lhs != nil) { if (lhs.kind == nkind.N_INDEX) { let idxbase: *node = lhs.lhs; @@ -9516,52 +9521,85 @@ fn cgdot(c: *cgen, n: *node) void = { if (lc != nil) { if (lc.tnode != nil) { let tn: *node = lc.tnode; let elemt: *node = nil; + let baseisarray: bool = false; let tk: nkind = tn.kind; if (tk == nkind.N_TSLICE) { elemt = tn.lhs; }; - if (tk == nkind.N_TARRAY) { elemt = tn.lhs; }; + if (tk == nkind.N_TARRAY) { elemt = tn.lhs; baseisarray = true; }; if (tk == nkind.N_TPTR) { elemt = tn.lhs; }; - if (elemt != nil) { if (elemt.kind == nkind.N_TPTR) { - let inner: *node = elemt.lhs; - if (inner != nil) { if (inner.kind == nkind.N_TNAME) { - let sname: str = inner.str; - let si: *structinfo = structlookup(c, sname); - if (si != nil) { - let fi: *fieldinfo = si.fields; - for (fi != nil) { - if (streq(fi.fname, fld)) { - cgexpr(c, lhs); // AX = *Struct - if (isstrtype(c, fi.tnode)) { - emitline("\tMOVQ\t"); - emitdispreg((fi.foff + 8): i64, "AX"); - emitline(", BX\n"); - emitline("\tMOVQ\t"); - emitdispreg(fi.foff: i64, "AX"); - emitline(", AX\n"); - return; - }; - if (isfloattype(c, fi.tnode)) { - let mov: str = "MOVSD"; - if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; - emitline("\t"); - emitline(mov); - emitline("\t"); - emitdispreg(fi.foff: i64, "AX"); - emitline(", X0\n"); - return; - }; - let lop: str = fieldloadop(c, fi); - emitline("\t"); - emitline(lop); - emitline("\t"); + let sname: str; + sname.ptr = nil; sname.len = 0; + let viaptr: bool = false; + if (elemt != nil) { + if (elemt.kind == nkind.N_TPTR) { + let inner: *node = elemt.lhs; + if (inner != nil) { if (inner.kind == nkind.N_TNAME) { + sname = inner.str; + viaptr = true; + };}; + } else { if (elemt.kind == nkind.N_TNAME) { + sname = elemt.str; + };}; + }; + if (sname.len > 0) { + let si: *structinfo = structlookup(c, sname); + if (si != nil) { + let fi: *fieldinfo = si.fields; + for (fi != nil) { + if (streq(fi.fname, fld)) { + let esz: i32 = elemsizeofc(c, tn); + cgexpr(c, lhs.rhs); // idx → AX + 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), AX\n"); + } else { + emitline("\tMOVQ\tBX, AX\n"); + }; + if (isstrtype(c, fi.tnode)) { + emitline("\tMOVQ\t"); + emitdispreg((fi.foff + 8): i64, "AX"); + emitline(", BX\n"); + emitline("\tMOVQ\t"); emitdispreg(fi.foff: i64, "AX"); emitline(", AX\n"); return; }; - fi = fi.finext; + if (isfloattype(c, fi.tnode)) { + let mov: str = "MOVSD"; + if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t"); + emitdispreg(fi.foff: i64, "AX"); + emitline(", X0\n"); + return; + }; + let lop: str = fieldloadop(c, fi); + emitline("\t"); + emitline(lop); + emitline("\t"); + emitdispreg(fi.foff: i64, "AX"); + emitline(", AX\n"); + return; }; + fi = fi.finext; }; - };}; - };}; + }; + }; };}; };}; }; diff --git a/test/wcc/680_arr_elem_field.c b/test/wcc/680_arr_elem_field.c new file mode 100644 index 00000000..7f66f4a7 --- /dev/null +++ b/test/wcc/680_arr_elem_field.c @@ -0,0 +1,232 @@ +/* + * 680_arr_elem_field — `arr[i].field` lowers to a single (idx*esz + + * base) → field-load sequence, not a fall-through that returns the + * element value (or address) as the dot's result. Filed from task #8 + * (originally framed as a wwstage bug; investigation flipped — cstage's + * N_DOT had no N_INDEX-lhs branch and fell through, leaving the + * caller's (AX, BX) shape junk for str/scalar leaf consumers). + * + * The cgenutil dotchainresolve workaround spills via `let nd = stk[i]; + * nd.str` to keep cstage from seeing the direct shape; this test pins + * the direct shape so the spill can be retired in a follow-up. + * + * Coverage — both `[N]*Struct` and `[N]Struct` shapes, str / i32 / u8 + * fields. Cstage and wwstage each on every fixture; wwstage gated on + * access(X_OK). + * + * `&arr[i].field` (TK_AMP through chained N_DOT N_INDEX) is task #9 + * territory and intentionally out of scope here. Write-side + * `arr[i].field = v` is covered elsewhere — the wwstage write gap + * surfaces separately. + */ +#include +#include +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return -1; +} + +struct row { const char *label; const char *src; int want; }; + +static const struct row rows[] = { + /* [N]*Struct, str field at non-zero offset. The original + * task-#8 repro — stk[0].name where stk: [16]*nd. + * Returns len("hello") = 5. */ + { "ptr_arr_str_field", + "type nd = struct {\n" + " a: i32, b: i32,\n" + " name: str,\n" + "};\n" + "fn main() i32 = {\n" + " let v: nd; v.a = 1; v.b = 2; v.name = \"hello\";\n" + " let stk: [16]*nd; stk[0] = &v;\n" + " let s: str = stk[0].name;\n" + " return s.len: i32;\n" + "};\n", + 5 }, + /* [N]*Struct, scalar i32 field. Pins the non-str leaf load + * path through fldloadop (MOVSXD here for i32). Returns 42. */ + { "ptr_arr_i32_field", + "type nd = struct { name: str, x: i32 };\n" + "fn main() i32 = {\n" + " let v: nd; v.name = \"hi\"; v.x = 42;\n" + " let stk: [16]*nd; stk[0] = &v;\n" + " return stk[0].x;\n" + "};\n", + 42 }, + /* [N]*Struct, u8 field — pins the sub-word zero-extend (MOVZBQ) + * branch. Returns 7. */ + { "ptr_arr_u8_field", + "type nd = struct { tag: u8, pad: u8, x: i32 };\n" + "fn main() i32 = {\n" + " let v: nd; v.tag = 7u8; v.pad = 0u8; v.x = 99;\n" + " let stk: [8]*nd; stk[0] = &v;\n" + " return stk[0].tag: i32;\n" + "};\n", + 7 }, + /* [N]Struct (value array), str field. Element address goes + * straight into AX; field load reads at foff(AX). Returns + * len("world") = 5. */ + { "val_arr_str_field", + "type nd = struct { name: str, x: i32 };\n" + "fn setit(p: *nd, s: str, vv: i32) void = { p.name = s; p.x = vv; };\n" + "fn main() i32 = {\n" + " let arr: [4]nd;\n" + " setit(&arr[1], \"world\", 100);\n" + " let s: str = arr[1].name;\n" + " return s.len: i32;\n" + "};\n", + 5 }, + /* [N]Struct, scalar i32 field. Pins the value-array path for a + * non-str leaf — same address compute, MOVL load. Returns 100. */ + { "val_arr_i32_field", + "type nd = struct { name: str, x: i32 };\n" + "fn setit(p: *nd, s: str, vv: i32) void = { p.name = s; p.x = vv; };\n" + "fn main() i32 = {\n" + " let arr: [4]nd;\n" + " setit(&arr[2], \"q\", 100);\n" + " return arr[2].x;\n" + "};\n", + 100 }, + /* [N]Struct, u8 leaf — pins the value-array sub-word path + * (no MOVQ-to-deref, direct address arithmetic into AX) then + * fldloadop's MOVZBQ. Returns 9. */ + { "val_arr_u8_field", + "type nd = struct { tag: u8, pad: u8, x: i32 };\n" + "fn setit(p: *nd, t: u8, vv: i32) void = { p.tag = t; p.x = vv; };\n" + "fn main() i32 = {\n" + " let arr: [4]nd;\n" + " setit(&arr[2], 9u8, 50);\n" + " return arr[2].tag: i32;\n" + "};\n", + 9 }, + /* [N]*Struct, i8 leaf with a negative value — pins that + * fldloadop sign-extends (MOVBQSX) through the new branch + * rather than the old MOVZBQ. Compare against -3 in i32 to + * avoid the WEXITSTATUS truncation collision (sign-ext: -3, + * zero-ext: 253 — both clash mod 256 if returned directly). */ + { "ptr_arr_i8_signed", + "type nd = struct { tag: i8, pad: u8, x: i32 };\n" + "fn main() i32 = {\n" + " let v: nd; v.tag = -3i8; v.pad = 0u8; v.x = 0;\n" + " let stk: [4]*nd; stk[0] = &v;\n" + " let t: i32 = stk[0].tag: i32;\n" + " if (t == -3) { return 42; };\n" + " return 0;\n" + "};\n", + 42 }, + /* Idx is a non-zero variable expression — pins the IMULQ path + * and confirms the address compute doesn't accidentally fold + * to a constant displacement. Returns len("third") = 5. */ + { "ptr_arr_dyn_idx", + "type nd = struct { name: str, x: i32 };\n" + "fn main() i32 = {\n" + " let a: nd; a.name = \"first\"; a.x = 1;\n" + " let b: nd; b.name = \"two\"; b.x = 2;\n" + " let c: nd; c.name = \"third\"; c.x = 3;\n" + " let stk: [4]*nd;\n" + " stk[0] = &a; stk[1] = &b; stk[2] = &c;\n" + " let i: i32 = 2;\n" + " let s: str = stk[i].name;\n" + " return s.len: i32;\n" + "};\n", + 5 }, +}; + +static int +run_driver(const char *driver, const struct row *r, int i) +{ + char src[64], tmpdir[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/wae_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/wae_%d_d_%d", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %s build %s", + tmpdir, driver, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: build via %s failed\n", + r->label, driver); + unlink(src); rmdir(tmpdir); + return -1; + } + + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + char outbin[128]; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + int got = runwait(outbin); + + unlink(src); unlink(outbin); rmdir(tmpdir); + return got; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char cdrv[1024]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + char wdrv[1024]; + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + struct { const char *name; const char *path; int gated_on_existence; } + drivers[] = { + { "cstage", cdrv, 0 }, + { "wwstage", wdrv, 1 }, + { NULL, NULL, 0 }, + }; + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + for (int d = 0; drivers[d].name; d++) { + if (drivers[d].gated_on_existence + && access(drivers[d].path, X_OK) != 0) { + fprintf(stderr, "arr_elem_field: skip %s (no %s)\n", + drivers[d].name, drivers[d].path); + continue; + } + for (int i = 0; i < n; i++) { + int got = run_driver(drivers[d].path, &rows[i], i); + total++; + if (got != rows[i].want) { + fprintf(stderr, + "arr_elem_field[%s][%s]: exit=%d want=%d\n", + drivers[d].name, rows[i].label, + got, rows[i].want); + fail++; + } + } + } + if (fail) { + fprintf(stderr, + "arr_elem_field: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("arr_elem_field: %d/%d ok\n", total, total); + return 0; +}