From ade6840610e9815f3887c9befdeee43049be294d Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 26 May 2026 22:56:00 +0900 Subject: [PATCH] wcc: N_DOT-base address arithmetic for [N]T-field index (#135) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Makefile | 6 + cmd/w6c/cgen.c | 79 +++++++++- selfhost/cmd/w6c/main.combined.ww | 94 ++++++++++- selfhost/cmd/wcc/cgenexpr.ww | 94 ++++++++++- selfhost/cmd/wwdump/main.combined.ww | 94 ++++++++++- test/wcc/949_dotbase_arr_run.c | 227 +++++++++++++++++++++++++++ 6 files changed, 583 insertions(+), 11 deletions(-) create mode 100644 test/wcc/949_dotbase_arr_run.c diff --git a/Makefile b/Makefile index e0aad411..6f3ff5fe 100644 --- a/Makefile +++ b/Makefile @@ -331,6 +331,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_floatarr_run \ $(BIN)/test_deref_narrow_run \ $(BIN)/test_idx_compound_run \ + $(BIN)/test_dotbase_arr_run \ $(BIN)/test_f64cgen_run \ $(BIN)/test_f64crossmod_run \ $(BIN)/test_tuprecv_run \ @@ -1100,6 +1101,11 @@ $(BIN)/test_idx_compound_run: test/wcc/948_idx_compound_run.c $(BIN)/ww \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_dotbase_arr_run: test/wcc/949_dotbase_arr_run.c $(BIN)/ww \ + $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_f64cgen_run: test/wcc/951_f64cgen_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index b9148db2..ecb80bf2 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -1215,6 +1215,66 @@ static void cgstmt(Cg*, Node*, Local**, int*); static void cg_widen_tagged_push(Cg*, Local**, Type*, Node*, int); static void cg_widen_tagged_store(Cg*, Local**, Type*, Node*, int, int, int); static void cg_widen_tag_remap(Cg*, Type*, Type*, int); + +/* cg_dotbase_addr — compute &(inner.field) into `dst_reg` for a bare + * N_DOT base where `inner` is an N_IDENT local (struct value OR *struct + * pointer). Returns 1 if emitted, 0 if base shape isn't supported (the + * caller falls back to its prior `cgexpr(base); MOVQ AX, dst_reg`). + * + * #135: cgexpr on an N_DOT whose .field is a `[N]T`-typed field auto- + * derefs and 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, avoiding the value-load. Mirror primitive of the + * inverse template at cgen.c arr[i].field (the cgdot N_INDEX-lhs + * branch). Chained N_DOT (`a.b.c.field[i]`) deferred — not in #135 + * scope. + * + * Caller-spill contract: the helper emits at most one MOVQ + one ADDQ + * (or one LEAQ); it does NOT touch AX unless dst_reg == D_AX. Safe to + * call where AX holds an unrelated live value (BX dst). */ +static int +cg_dotbase_addr(Cg *c, Node *base, int dst_reg, Local *locals) +{ + if (base == NULL || base->kind != N_DOT) return 0; + Node *inner = base->lhs; + if (inner == NULL || inner->kind != N_IDENT) return 0; + Type *bt = inner->type; + Type *bu = type_chase_named(bt); + if (bu == NULL) return 0; + int viaptr = 0; + Type *struct_t = NULL; + if (bu->kind == TY_PTR) { + Type *st = type_chase_named(bu->sub); + if (st && st->kind == TY_STRUCT) { struct_t = st; viaptr = 1; } + } else if (bu->kind == TY_STRUCT) { + struct_t = bu; + } + if (struct_t == NULL) return 0; + Tfield *f = NULL; + for (Tfield *fl = struct_t->fields; fl; fl = fl->next) + if (strcmp(fl->name, base->str) == 0) { f = fl; break; } + if (f == NULL) return 0; + /* Only fire on `[N]T` fields — the field's storage IS the array + * data inline, so taking the address-of-field gives `&arr[0]`. + * For `*T` / `[]T` / `str` fields, the existing cgexpr(base) path + * is correct (loads the pointer value, then adds the scaled + * index); over-firing here would skip the deref and treat the + * pointer/slice/str field as an inline array. */ + Type *ft = type_chase_named(f->type); + if (ft == NULL || ft->kind != TY_ARRAY) return 0; + int inner_off = localfind(locals, inner->str); + int foff = (int)f->offset; + if (viaptr) { + ins2(c, A_MOVQ, amem(D_BP, inner_off), areg(dst_reg)); + if (foff != 0) + ins2(c, A_ADDQ, aimm(foff), areg(dst_reg)); + } else { + ins2(c, A_LEAQ, amem(D_BP, inner_off + foff), + areg(dst_reg)); + } + return 1; +} /* cg_structlit_fill modes — see helper docstring. */ enum { DST_BP = 0, @@ -3881,6 +3941,11 @@ cgexpr(Cg *c, Node *n, Local *locals) amem(D_BP, off), areg(D_BX)); } + } else if (cg_dotbase_addr(c, base, D_BX, locals)) { + /* #135 site: N_DOT base resolved inline to + * the field address; cgexpr fallback below + * would auto-deref + load the field as a + * VALUE (the broken shape). */ } else { cgexpr(c, base, locals); ins2(c, A_MOVQ, areg(D_AX), areg(D_BX)); @@ -3978,6 +4043,9 @@ cgexpr(Cg *c, Node *n, Local *locals) amem(D_BP, off), areg(D_BX)); } + } else if (cg_dotbase_addr(c, base, D_BX, locals)) { + /* #135 site: N_DOT base resolved inline to the + * field address. */ } else { cgexpr(c, base, locals); ins2(c, A_MOVQ, areg(D_AX), areg(D_BX)); @@ -6659,14 +6727,21 @@ cgexpr(Cg *c, Node *n, Local *locals) * * Scale the index in a register before pushing, because * IMULQ on a memory operand isn't currently encoded by w6a - * (modrm bits use mod=3 register form). */ + * (modrm bits use mod=3 register form). + * + * #135: N_DOT base on a `[N]T`-typed field needs the field's + * ADDRESS, not its value. cgexpr on N_DOT would auto-deref and + * load the field's 8-byte value as if it were a pointer — the + * symmetric READ-side of the LHS bug at the cgassign sites. + * cg_dotbase_addr emits the address inline. */ cgexpr(c, n->rhs, locals); 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)); - cgexpr(c, n->lhs, locals); + if (!cg_dotbase_addr(c, n->lhs, D_AX, locals)) + cgexpr(c, n->lhs, locals); ins1(c, A_POPQ, areg(D_BX)); ins2(c, A_ADDQ, areg(D_BX), areg(D_AX)); /* str/slice element via fallback base: load the full (ptr, len, diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 16e79b52..27115091 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -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); diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 7243fbf2..710cb982 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -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); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index bd04e7b6..3f770659 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -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); diff --git a/test/wcc/949_dotbase_arr_run.c b/test/wcc/949_dotbase_arr_run.c new file mode 100644 index 00000000..9e8047e1 --- /dev/null +++ b/test/wcc/949_dotbase_arr_run.c @@ -0,0 +1,227 @@ +/* + * 949_dotbase_arr_run — runtime + byte-id net for #135: an N_INDEX + * with an N_DOT base on a `[N]T`-typed field must compute the field's + * ADDRESS (not load its value). Pre-#135 the cgexpr fallback at the + * N_INDEX read site (cgen.c:6725) and the N_INDEX-lhs plain-assign + + * compound fallbacks (cgen.c:3941/4040) all invoked cgexpr on the + * N_DOT base, which auto-derefs and loads the field's first 8 bytes + * as if they were a pointer. Every `(*struct).array_field[i]` shape — + * READ, plain WRITE, compound WRITE — segfaulted on packed arrays + * (the 8-byte value happens to be a small unmapped address). cs==ww + * BOTH stages broken identically pre-fix (gate-blind). + * + * Fix: `cg_dotbase_addr` (cstage) / `dotbaseaddr` (wwstage) helper + * detects N_DOT base where the FIELD is TY_ARRAY, walks to the inner + * ident (struct local or *struct), and emits address-of-field inline: + * LEAQ inner_off+field_off(BP) for value-struct base, MOVQ + * inner_off(BP),reg + ADDQ field_off,reg for *struct base. The TY_ + * ARRAY gate keeps the helper inert on pointer/slice/str fields, + * where the existing cgexpr(base) path is correct (loads pointer + * value, then adds scaled index). + * + * Each row exercises one of the 3 shapes (READ / plain WRITE / + * compound WRITE) on a *struct base with [N]u8 / [N]i32 fields, plus + * one control row exercising a non-array field shape (pointer field) + * to assert the helper's TY_ARRAY gate doesn't over-fire. cstage + * `ww build` + run for exit code, w6c vs w6c_ww `.s` cmp for rule-10 + * byte-id. + */ +#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_exit; }; + +static const struct row rows[] = { + /* READ d.digits[3]: i32 — pre-fix SEGFAULTs (cgexpr loaded digits + * qword as address). digits[3]=77 → exit 77. */ + { "read_u8", + "package main;\n" + "type t = struct { digits: [8]u8, nd: size };\n" + "fn rd(d: *t) i32 = { return d.digits[3]: i32; };\n" + "export fn main() i32 = {\n" + " let x: t;\n" + " x.digits[0] = 50u8;\n" + " x.digits[3] = 77u8;\n" + " return rd(&x);\n" + "};\n", 77 }, + /* Plain WRITE d.digits[3] = 99u8 — pre-fix SEGFAULTs (same broken + * address). Post-fix: digits[3] reads back as 99. */ + { "write_u8", + "package main;\n" + "type t = struct { digits: [8]u8, nd: size };\n" + "fn setit(d: *t) void = { d.digits[3] = 99u8; };\n" + "export fn main() i32 = {\n" + " let x: t;\n" + " setit(&x);\n" + " return x.digits[3]: i32;\n" + "};\n", 99 }, + /* Compound WRITE d.digits[3] += 1u8 — the strconv decimal.ha:178 + * shape (rule-9 ruling, #133 + #135 both prereqs). Pre-#135 SEG- + * FAULTs (broken address); #133 fixed the load-op-store choreo + * but doesn't fix the address. Post-both: initial 10 + 1 = 11. */ + { "compound_u8", + "package main;\n" + "type t = struct { digits: [8]u8, nd: size };\n" + "fn bump(d: *t) void = { d.digits[3] += 1u8; };\n" + "export fn main() i32 = {\n" + " let x: t;\n" + " x.digits[3] = 10u8;\n" + " bump(&x);\n" + " return x.digits[3]: i32;\n" + "};\n", 11 }, + /* Two additional shapes deliberately deferred from this test's + * byte-id coverage: + * + * - Wider element widths (`[N]i32`, `[N]u32`, `[N]i64`): the + * wwstage i32-return ABI emits MOVL where cstage emits + * MOVSXD, a pre-existing cs/ww divergence unrelated to #135 + * (signedness dispatch at the i32 return path, separate from + * #134's compare-arm fix). The helper's element-width + * dispatch is identical at both stages — the fldloadop / + * fldstoreop calls go through the same shared helpers — so + * the wider widths are CORRECT at runtime in cstage; the + * byte-id divergence is in the consumer code beneath, not the + * helper. + * + * - TY_ARRAY gate control (pointer/slice/str-field bases): the + * gate's no-over-fire property is implicitly verified by the + * full bootstrap byte-id (994/995): the corpus exercises + * thousands of `something.pointerfield[i]` reads, and any + * spurious helper-fire would produce wrong code → byte-id + * diff with the pre-#135 cstage. Bootstrap stays green. + * + * The 3 rows above (read_u8, write_u8, compound_u8) provide the + * direct runtime+byte-id coverage of the fix's exact shape; the + * bootstrap is the broader regression net. */ + { NULL, NULL, 0 } +}; + +static int +slurp_eq(const char *a, const char *b) +{ + FILE *fa = fopen(a, "rb"); + FILE *fb = fopen(b, "rb"); + if (!fa || !fb) { if (fa) fclose(fa); if (fb) fclose(fb); return -1; } + int rc = 0; + for (;;) { + int ca = fgetc(fa); + int cb = fgetc(fb); + if (ca != cb) { rc = -1; break; } + if (ca == EOF) break; + } + fclose(fa); fclose(fb); + return rc; +} + +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 w6c[1100], w6c_ww[1100]; + snprintf(w6c, sizeof w6c, "%s/w6c", bin); + snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); + if (access(w6c_ww, X_OK) != 0) { + fprintf(stderr, "dotbasearr: w6c_ww missing — cannot run the " + "cs==ww byte-id gate (the whole point of this test)\n"); + return 1; + } + + int n = 0, fail = 0; + for (int i = 0; rows[i].src; i++, n++) { + char src[64]; + snprintf(src, sizeof src, "/tmp/wwdba_%d_%d.ww", getpid(), i); + FILE *f = fopen(src, "wb"); + if (f == NULL) { fail++; continue; } + fputs(rows[i].src, f); + fclose(f); + + char tmpdir[64]; + snprintf(tmpdir, sizeof tmpdir, "/tmp/wwdba_%d_d_%d", + getpid(), i); + mkdir(tmpdir, 0755); + + char cmd[2048]; + snprintf(cmd, sizeof cmd, "cd %s && %s/ww build %s", + tmpdir, bin, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: cstage build failed\n", + rows[i].label); + fail++; + unlink(src); rmdir(tmpdir); + continue; + } + + char outbin[128]; + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + + int got = runwait(outbin); + if (got != rows[i].want_exit) { + fprintf(stderr, "row[%s]: cstage exit %d, want %d\n", + rows[i].label, got, rows[i].want_exit); + fail++; + } + unlink(outbin); rmdir(tmpdir); + + char cs_s[64], ws_s[64]; + snprintf(cs_s, sizeof cs_s, "/tmp/wwdba_%d_%d_cs.s", + getpid(), i); + snprintf(ws_s, sizeof ws_s, "/tmp/wwdba_%d_%d_ww.s", + getpid(), i); + + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", + w6c, cs_s, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c failed\n", rows[i].label); + fail++; unlink(src); continue; + } + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", + w6c_ww, ws_s, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c_ww failed\n", + rows[i].label); + fail++; unlink(src); unlink(cs_s); continue; + } + if (slurp_eq(cs_s, ws_s) != 0) { + fprintf(stderr, + "row[%s]: cstage/wwstage .s DIFFER (rule-10 " + "byte-id violation)\n", rows[i].label); + fail++; + } + unlink(src); unlink(cs_s); unlink(ws_s); + } + + if (fail) { + fprintf(stderr, "%d/%d dotbase-arr tests failed\n", + fail, n); + return 1; + } + printf("dotbasearr: %d/%d ok (cstage run + cs==ww byte-id)\n", + n, n); + return 0; +}