diff --git a/Makefile b/Makefile index 8992eba9..9e41f58e 100644 --- a/Makefile +++ b/Makefile @@ -372,6 +372,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_len_strglobal_run \ $(BIN)/test_litstr_pseudo_run \ $(BIN)/test_lenidx_run \ + $(BIN)/test_globalidx_run \ $(BIN)/test_tuple_sret_callee \ $(BIN)/test_tuple_sret_receive_run \ $(BIN)/test_struct_tuple_field_slot \ @@ -902,6 +903,17 @@ $(BIN)/test_lenidx_run: test/wcc/802_lenidx_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# #10: indexing a GLOBAL str / GLOBAL slice (`s[i]` / `g[i]`) read a wide +# {ptr,len,cap} header with an 8-byte stride + full-word load instead of the +# element load — wwstage `cgindex` dispatched esz off the base tnode KIND +# (only N_TARRAY/N_TPTR), so a global str/slice matched neither and defaulted +# to esz=8. cstage is correct (type-driven dispatch). Runtime (cstage build+ +# run) + cs==ww byte-id, both dimensions per row. +$(BIN)/test_globalidx_run: test/wcc/803_globalidx_run.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + # #10 Fold A (wide tuple-return / sret, CALLEE side): an over-cap tuple # return (> 4 GP or > 2 SSE eightbytes) now compiles via sret instead of # loud-stopping at the SEND. Compile + cs==ww byte-id only — the receive diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index e6f3b870..b5d00b28 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -20980,23 +20980,35 @@ fn cgindex(c: *cgen, n: *node) void = { // A.2 cgdot widening site). if (tn == nil) { tn = defvartnode(c, bn); }; if (tn != nil) { + // #10: dispatch esz + base-materialization off the + // global's RESOLVED type via elemsizeofc, NOT an + // N_TARRAY/N_TPTR kind whitelist. A global str (tnode + // N_TNAME "str") / slice (N_TSLICE) matched NEITHER old + // arm, so esz stayed at the default 8 and the base fell + // to the wide-header fallback below (8B stride + full- + // word MOVQ) instead of loading the .ptr + an element- + // width load. cstage dispatches uniformly off + // idx_eff(lhs->type)->sub->size (cmd/w6c/cgen.c + // N_INDEX); the sister fn cgslice (this file) already + // resolves esz via elemsizeofc and the base via + // N_TARRAY?LEAQ:MOVQ name(SB) with no kind gate. Align + // cgindex UP to that template: any indexable global + // resolves esz off the type table, N_TARRAY -> LEAQ (the + // symbol IS the storage), every other -> MOVQ name(SB) + // (the symbol's first word IS the .ptr). The existing + // isglobalptr emission (the loadopsz path below) then + // yields the cstage-identical MOVZBQ for a str byte + // (esz=1). + globalname = bn; + esz = elemsizeofc(c, tn); + signed_elem = elemissignedc(c, tn); + float_elem = elemisfloatc(c, tn); + f32_elem = elemisf32c(c, tn); + elem_isarray = elemisarrayc(c, tn); if (tn.kind == nkind.N_TARRAY) { isglobalarr = true; - globalname = bn; - esz = elemsizeofc(c, tn); - signed_elem = elemissignedc(c, tn); - float_elem = elemisfloatc(c, tn); - f32_elem = elemisf32c(c, tn); - elem_isarray = elemisarrayc(c, tn); - }; - if (tn.kind == nkind.N_TPTR) { + } else { isglobalptr = true; - globalname = bn; - esz = elemsizeofc(c, tn); - signed_elem = elemissignedc(c, tn); - float_elem = elemisfloatc(c, tn); - f32_elem = elemisf32c(c, tn); - elem_isarray = elemisarrayc(c, tn); }; }; }; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 17952a9a..56a21e32 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -1256,23 +1256,35 @@ fn cgindex(c: *cgen, n: *node) void = { // A.2 cgdot widening site). if (tn == nil) { tn = defvartnode(c, bn); }; if (tn != nil) { + // #10: dispatch esz + base-materialization off the + // global's RESOLVED type via elemsizeofc, NOT an + // N_TARRAY/N_TPTR kind whitelist. A global str (tnode + // N_TNAME "str") / slice (N_TSLICE) matched NEITHER old + // arm, so esz stayed at the default 8 and the base fell + // to the wide-header fallback below (8B stride + full- + // word MOVQ) instead of loading the .ptr + an element- + // width load. cstage dispatches uniformly off + // idx_eff(lhs->type)->sub->size (cmd/w6c/cgen.c + // N_INDEX); the sister fn cgslice (this file) already + // resolves esz via elemsizeofc and the base via + // N_TARRAY?LEAQ:MOVQ name(SB) with no kind gate. Align + // cgindex UP to that template: any indexable global + // resolves esz off the type table, N_TARRAY -> LEAQ (the + // symbol IS the storage), every other -> MOVQ name(SB) + // (the symbol's first word IS the .ptr). The existing + // isglobalptr emission (the loadopsz path below) then + // yields the cstage-identical MOVZBQ for a str byte + // (esz=1). + globalname = bn; + esz = elemsizeofc(c, tn); + signed_elem = elemissignedc(c, tn); + float_elem = elemisfloatc(c, tn); + f32_elem = elemisf32c(c, tn); + elem_isarray = elemisarrayc(c, tn); if (tn.kind == nkind.N_TARRAY) { isglobalarr = true; - globalname = bn; - esz = elemsizeofc(c, tn); - signed_elem = elemissignedc(c, tn); - float_elem = elemisfloatc(c, tn); - f32_elem = elemisf32c(c, tn); - elem_isarray = elemisarrayc(c, tn); - }; - if (tn.kind == nkind.N_TPTR) { + } else { isglobalptr = true; - globalname = bn; - esz = elemsizeofc(c, tn); - signed_elem = elemissignedc(c, tn); - float_elem = elemisfloatc(c, tn); - f32_elem = elemisf32c(c, tn); - elem_isarray = elemisarrayc(c, tn); }; }; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 4c0e363c..5a872731 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -20980,23 +20980,35 @@ fn cgindex(c: *cgen, n: *node) void = { // A.2 cgdot widening site). if (tn == nil) { tn = defvartnode(c, bn); }; if (tn != nil) { + // #10: dispatch esz + base-materialization off the + // global's RESOLVED type via elemsizeofc, NOT an + // N_TARRAY/N_TPTR kind whitelist. A global str (tnode + // N_TNAME "str") / slice (N_TSLICE) matched NEITHER old + // arm, so esz stayed at the default 8 and the base fell + // to the wide-header fallback below (8B stride + full- + // word MOVQ) instead of loading the .ptr + an element- + // width load. cstage dispatches uniformly off + // idx_eff(lhs->type)->sub->size (cmd/w6c/cgen.c + // N_INDEX); the sister fn cgslice (this file) already + // resolves esz via elemsizeofc and the base via + // N_TARRAY?LEAQ:MOVQ name(SB) with no kind gate. Align + // cgindex UP to that template: any indexable global + // resolves esz off the type table, N_TARRAY -> LEAQ (the + // symbol IS the storage), every other -> MOVQ name(SB) + // (the symbol's first word IS the .ptr). The existing + // isglobalptr emission (the loadopsz path below) then + // yields the cstage-identical MOVZBQ for a str byte + // (esz=1). + globalname = bn; + esz = elemsizeofc(c, tn); + signed_elem = elemissignedc(c, tn); + float_elem = elemisfloatc(c, tn); + f32_elem = elemisf32c(c, tn); + elem_isarray = elemisarrayc(c, tn); if (tn.kind == nkind.N_TARRAY) { isglobalarr = true; - globalname = bn; - esz = elemsizeofc(c, tn); - signed_elem = elemissignedc(c, tn); - float_elem = elemisfloatc(c, tn); - f32_elem = elemisf32c(c, tn); - elem_isarray = elemisarrayc(c, tn); - }; - if (tn.kind == nkind.N_TPTR) { + } else { isglobalptr = true; - globalname = bn; - esz = elemsizeofc(c, tn); - signed_elem = elemissignedc(c, tn); - float_elem = elemisfloatc(c, tn); - f32_elem = elemisf32c(c, tn); - elem_isarray = elemisarrayc(c, tn); }; }; }; diff --git a/test/wcc/803_globalidx_run.c b/test/wcc/803_globalidx_run.c new file mode 100644 index 00000000..a2b984ed --- /dev/null +++ b/test/wcc/803_globalidx_run.c @@ -0,0 +1,266 @@ +/* + * 803_globalidx_run — BUG #10. Runtime + cs==ww byte-id net for INDEXING + * a GLOBAL `str` / GLOBAL slice (`s[i]` / `g[i]` where s/g are module-level + * lets). + * + * THE BUG (wwstage WRONG, cstage correct — a rule-10 divergence): + * wwstage `cgindex` (selfhost/cmd/wcc/cgenexpr.ww) dispatched the element + * size + base-materialisation off the base's tnode KIND, enumerating only + * N_TARRAY (global `[N]T`) and N_TPTR (global `*T`). A global str (tnode + * N_TNAME "str") and a global slice (N_TSLICE) matched NEITHER arm, so esz + * stayed at the default 8 and the base fell to the wide-header fallback — + * it materialised the full {ptr,len,cap} header and indexed with an 8-byte + * stride + a full-word MOVQ load. So `s[1]` over a global str read 8 bytes + * at ptr+8 instead of the single byte at ptr+1 (cstage emits MOVZBQ). + * cstage `case N_INDEX:` (cmd/w6c/cgen.c) dispatches esz off the RESOLVED + * base TYPE (`idx_eff(lhs->type)->sub->size`), uniform across local/global/ + * str/slice/ptr — so it was already correct. LOCAL str/slice index was also + * already byte-id-clean (esz resolves off the local's tnode). + * + * THE FIX (#10): align cgindex's global-resolution arm UP to cstage's uniform + * type-driven dispatch — the same template the sister fn `cgslice` already + * uses (generic globaltn, esz = elemsizeofc(c, tn), base = N_TARRAY ? LEAQ : + * MOVQ name(SB)). A global str/slice now resolves esz=1 off the type table + * and routes through the existing isglobalptr emission (MOVQ name(SB),BX; + * ADDQ; MOVZBQ (BX),AX) — byte-identical to cstage. + * + * EACH ROW CARRIES BOTH DIMENSIONS (802 model): + * (a) cstage `ww build` + run, asserting the exit — pins that the converged + * asm reads the correct element value, not a ptr word. + * (b) w6c vs w6c_ww `.s` cmp — FAILS if the stages diverge (rule-10). A + * stride-8 regression of the fix re-diverges wwstage from cstage here. + * + * BYTE-ID SCOPE PER ROW: + * Most rows compare the FULL `.s`. The global-slice row compares the TEXT + * section only (lines before the first DATA/GLOBL directive): a bare + * module-level `let g: []u8;` decl emits a divergent zero-header DATAW in + * wwstage that cstage omits — a SEPARATE, pre-existing data-emission gap + * (module-level slice static-init, task #7/#18 family) orthogonal to the + * index read. The TEXT comparison still pins the index codegen exactly + * (where a stride-8 regression manifests: MOVZBQ vs IMULQ $8 + MOVQ), so + * the #10 fix is fully gated; only the unrelated DATA noise is excluded. + * (Same kind of orthogonal block 802 documents for its slice rows.) + * + * GATE POLARITY: must stay GREEN. A wrong exit means a global str/slice index + * regressed back to a ptr-word read; a byte-id FAIL means the stages diverged. + */ +#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; int text_only; }; + +static const struct row rows[] = { + /* the exact #10 repro: a global str "ABC", s[1] == 'B' == 66. + * Pre-fix this read an 8-byte word at ptr+8 (wide-header stride). */ + { "gstr_mid", + "package main;\n" + "let s: str = \"ABC\";\n" + "export fn main() i32 = { return s[1]: i32; };\n", 66, 0 }, + /* first element s[0] == 'A' == 65 — guards an off-by-one in the + * index scale (a wide stride would land elsewhere). */ + { "gstr_first", + "package main;\n" + "let s: str = \"ABC\";\n" + "export fn main() i32 = { return s[0]: i32; };\n", 65, 0 }, + /* last element s[2] == 'C' == 67. */ + { "gstr_last", + "package main;\n" + "let s: str = \"ABC\";\n" + "export fn main() i32 = { return s[2]: i32; };\n", 67, 0 }, + /* two global-str indices summed (65 + 66 == 131) — pins the byte + * load width: a full-word load would carry the high bytes. */ + { "gstr_sum", + "package main;\n" + "let s: str = \"ABC\";\n" + "export fn main() i32 = { return s[0]: i32 + s[1]: i32; };\n", 131, 0 }, + /* global slice index: g[1] == 20. The bare `let g: []u8;` decl emits + * a divergent zero-header DATAW (orthogonal #7/#18 data gap), so this + * row is TEXT-only byte-id; cstage runtime pins the value, TEXT-id + * pins the index read. g is assigned at runtime to dodge the SEPARATE + * module-level slice-literal static-init gap (which fails to link in + * BOTH stages identically). */ + { "gslice_mid", + "package main;\n" + "let g: []u8;\n" + "export fn main() i32 = {\n" + " let a: [3]u8 = [10u8, 20u8, 30u8];\n" + " g = a;\n" + " return g[1]: i32;\n" + "};\n", 20, 1 }, + /* global slice with a WIDTH>1, SIGNED element: g[1] - g[0] == 20 - (-5) + * == 25. Pins that the global path resolves esz=4 off the type table + * (not the default 8) AND sign-extends (MOVSXD) — a stride-8 regression + * re-diverges the TEXT (IMULQ $8 + MOVQ word vs IMULQ $4 + MOVSXD). Same + * bare-decl DATAW data-gap as gslice_mid, so TEXT-only. */ + { "gislice_mid", + "package main;\n" + "let g: []i32;\n" + "export fn main() i32 = {\n" + " let a: [3]i32 = [-5, 20, 30];\n" + " g = a;\n" + " return g[1] - g[0];\n" + "};\n", 25, 1 }, + /* REGRESSION PIN: local str index (already byte-id-clean pre-fix) — + * the fix must not perturb the local path. s[1] == 'B' == 66. */ + { "lstr_mid", + "package main;\n" + "export fn main() i32 = { let s: str = \"ABC\"; return s[1]: i32; };\n", + 66, 0 }, + /* REGRESSION PIN: local slice index (already clean). g[2] == 30. */ + { "lslice_last", + "package main;\n" + "export fn main() i32 = {\n" + " let a: [3]u8 = [10u8, 20u8, 30u8];\n" + " let g: []u8 = a;\n" + " return g[2]: i32;\n" + "};\n", 30, 0 }, + /* REGRESSION PIN: local array index (already clean). a[1] == 20. */ + { "larr_mid", + "package main;\n" + "export fn main() i32 = {\n" + " let a: [3]u8 = [10u8, 20u8, 30u8];\n" + " return a[1]: i32;\n" + "};\n", 20, 0 }, + { NULL, NULL, 0, 0 } +}; + +/* A directive line that begins the DATA/GLOBL section. The TEXT-only + * compare stops at the first such line (the index codegen lives entirely + * in the TEXT segment above it). */ +static int +isdataline(const char *ln) +{ + return strncmp(ln, "DATA", 4) == 0 || strncmp(ln, "GLOBL", 5) == 0; +} + +/* Byte-compare two .s files. With text_only, both files are truncated at + * the first DATA/GLOBL line before comparison (orthogonal data-section + * divergence excluded; see the BYTE-ID SCOPE note above). */ +static int +asm_eq(const char *a, const char *b, int text_only) +{ + 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; + char la[4096], lb[4096]; + for (;;) { + char *ra = fgets(la, sizeof la, fa); + char *rb = fgets(lb, sizeof lb, fb); + if (text_only && ra && isdataline(la)) ra = NULL; + if (text_only && rb && isdataline(lb)) rb = NULL; + if (ra == NULL && rb == NULL) break; + if (ra == NULL || rb == NULL) { rc = -1; break; } + if (strcmp(la, lb) != 0) { rc = -1; 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, "globalidx: w6c_ww missing — cannot run the " + "cs==ww byte-id gate\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/wwgi_%d_%d.ww", getpid(), i); + FILE *f = fopen(src, "wb"); + if (f == NULL) { fail++; continue; } + fputs(rows[i].src, f); + fclose(f); + + /* (a) cstage build + run in a scratch dir. */ + char tmpdir[64]; + snprintf(tmpdir, sizeof tmpdir, "/tmp/wwgi_%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); + + /* (b) cs==ww byte-id gate. */ + char cs_s[64], ws_s[64]; + snprintf(cs_s, sizeof cs_s, "/tmp/wwgi_%d_%d_cs.s", getpid(), i); + snprintf(ws_s, sizeof ws_s, "/tmp/wwgi_%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 (asm_eq(cs_s, ws_s, rows[i].text_only) != 0) { + fprintf(stderr, "row[%s]: cstage/wwstage .s DIFFER%s " + "(rule-10 byte-id violation)\n", rows[i].label, + rows[i].text_only ? " (TEXT section)" : ""); + fail++; + } + unlink(src); unlink(cs_s); unlink(ws_s); + } + + if (fail) { + fprintf(stderr, "%d/%d globalidx tests failed\n", fail, n); + return 1; + } + printf("globalidx: %d/%d ok (cstage run + cs==ww byte-id)\n", n, n); + return 0; +}