diff --git a/Makefile b/Makefile index 4e1207b5..d84e3f05 100644 --- a/Makefile +++ b/Makefile @@ -513,6 +513,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_structvariant_largeunion_return \ $(BIN)/test_narrow_alias_deref_store \ $(BIN)/test_taggedderefstore_run \ + $(BIN)/test_taggedglobalindex_run \ $(BIN)/test_bufio_vstream_run \ $(BIN)/test_log_vstream_run \ $(BIN)/test_use_promote_alias \ @@ -839,6 +840,23 @@ $(BIN)/test_taggedderefstore_run: test/wcc/989_taggedderefstore_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# 989_taggedglobalindex_run (#16): an indexed store into a GLOBAL tagged array +# `gs[i] = v` (gs:[N](A|B)) — the indexed-tagged-element assign arm (cmd/w6c/ +# cgen.c ~6502) lacked the `isglobal -> LEAQ name(SB)` base branch the scalar +# `arr[i]=v` element arm (~6767) already had, so the global base resolved to a +# junk BP offset and the widened store landed nowhere; cstage silently returned +# 0 while wwstage (cgenexpr.ww index arm, already global-aware) returned the +# correct value — a cs!=ww divergence the byte-id gate could SEE. #16 aligns +# cstage UP with the mirrored isglobal branch. Builds+runs on BOTH driver twins +# (rule-10), pinning the absolute 0; loc_ctl + scalar_ctl pin the unchanged +# frame-base and scalar-template paths. +$(BIN)/test_taggedglobalindex_run: test/wcc/989_taggedglobalindex_run.c \ + $(BIN)/ww $(BIN)/ww_ww \ + $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + # 989_arrlit_tail_zero_run (#13): an under-length array literal zero-fills the # unspecified tail, not the last value. Builds+runs on BOTH driver twins # (rule-10), pinning the absolute value (pre-fix ww tail = last value). diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index fdcf2775..0c2cde8e 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -6499,14 +6499,27 @@ cgexpr(Cg *c, Node *n, Local *locals) ins2(c, A_MOVQ, aimm(ssz), areg(D_CX)); ins2(c, A_IMULQ, areg(D_CX), areg(D_AX)); } - if (base->kind == N_IDENT && is_arr) { + if (base->kind == N_IDENT) { int boff = localfind(locals, base->str); - ins2(c, A_LEAQ, amem(D_BP, boff), - areg(D_BX)); - } else if (base->kind == N_IDENT) { - int boff = localfind(locals, base->str); - ins2(c, A_MOVQ, amem(D_BP, boff), - areg(D_BX)); + int isglobal = (boff == 0) && + let_islet(base->str); + if (isglobal && is_arr) { + ins2(c, A_LEAQ, + masym(c, base->str), + areg(D_BX)); + } else if (isglobal) { + ins2(c, A_MOVQ, + masym(c, base->str), + areg(D_BX)); + } else if (is_arr) { + ins2(c, A_LEAQ, + amem(D_BP, boff), + areg(D_BX)); + } else { + ins2(c, A_MOVQ, + amem(D_BP, boff), + areg(D_BX)); + } } else if (cg_dotbase_addr(c, base, D_BX, locals)) { /* #259: N_DOT base resolved inline to the * field address; cgexpr fallback would diff --git a/test/wcc/989_taggedglobalindex_run.c b/test/wcc/989_taggedglobalindex_run.c new file mode 100644 index 00000000..54a5599b --- /dev/null +++ b/test/wcc/989_taggedglobalindex_run.c @@ -0,0 +1,254 @@ +/* + * 989_taggedglobalindex_run — #16 teeth (recorded by impl-16, 2026-06-14). + * + * Indexed store into a GLOBAL array of a tagged-union type: `gs[i] = v` where + * gs : [N](A | B). The indexed-tagged-element assign arm (cmd/w6c/cgen.c + * ~6502) computed its base address with only LOCAL/frame variants — it LACKED + * the `isglobal -> LEAQ name(SB)` branch the SCALAR `arr[i] = v` element arm + * (~6767) already had. For a global tagged array the base resolved to a junk + * BP-relative offset (off==0), so the widened store landed nowhere observable; + * a later `match (gs[i])` then read the unchanged static-init slot. cstage + * silently returned 0 (WRONG); wwstage (cgenexpr.ww index arm, already carries + * the globalname branch) returned the correct value — a cs!=ww divergence the + * byte-id gate could SEE. #16 aligns cstage UP: it adds the mirrored + * `isglobal && is_arr -> LEAQ name(SB)` / `isglobal -> MOVQ name(SB)` branch to + * the indexed-tagged-elem arm. cstage-only; no .ww source touched. + * + * Rows (each program self-checks and returns 0 on all-correct, a 1-based code + * on the first mismatch; both stages build+run, rule-10, pin 0): + * glo_taglo: global [N](int|bool), gs[1]=42 — tag-low (int) variant, read + * back 42. The literal #16 teeth (cstage pre-fix gave 0). + * glo_taghi: global [N](int|bool), gs[1]=true — tag-high (bool) variant; + * the tag must remap through the widener. + * glo_varidx: global [N](int|bool), runtime index gs[i]=99 — the scaled- + * index path (IMULQ) layered on the global base. + * glo_str3w: global [N](int|str), gs[1]="hello" — a str payload is a + * 3-word {ptr,len,cap} header; proves the multi-word slot copy + * over the global base is COMPLETE, not just word0/word1. + * glo_struct2w:global [N](int|pair), gs[1]=pair{...} — a 16B struct payload + * (slot = tag + 2 payload words); proves the mid-count copy loop + * moves BOTH payload words over the global base. + * loc_ctl: LOCAL [N](int|bool), a[1]=42 — control for the frame-base + * path that ALREADY worked; must stay correct + byte-id. + * scalar_ctl: scalar global [N]int, arr[1]=42 — control for the template + * arm (the scalar element store) the fix was mirrored from. + */ +#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; /* >= 0: pin the absolute value; -1: cs==ww only */ +}; + +static const struct row rows[] = { + /* glo_taglo — the #16 teeth: global tagged array, int variant store. */ + { "glo_taglo", + "package main;\n" + "let gs: [2](int | bool) = [0, 0];\n" + "export fn main() int = {\n" + " gs[1] = 42;\n" + " match (gs[1]) {\n" + " case let n: int => { if (n != 42) { return 1; }; return 0; };\n" + " case bool => { return 2; };\n" + " };\n" + "};\n", + 0 }, + + /* glo_taghi — bool variant via gs[1]=true; the tag must remap. */ + { "glo_taghi", + "package main;\n" + "let gs: [2](int | bool) = [0, 0];\n" + "export fn main() int = {\n" + " gs[1] = true;\n" + " match (gs[1]) {\n" + " case int => { return 1; };\n" + " case let b: bool => { if (!b) { return 2; }; return 0; };\n" + " };\n" + "};\n", + 0 }, + + /* glo_varidx — runtime index over the global base (scaled IMULQ). */ + { "glo_varidx", + "package main;\n" + "let gs: [3](int | bool) = [0, 0, 0];\n" + "export fn main() int = {\n" + " let i: int = 2;\n" + " gs[i] = 99;\n" + " match (gs[2]) {\n" + " case let n: int => { if (n != 99) { return 1; }; return 0; };\n" + " case bool => { return 2; };\n" + " };\n" + "};\n", + 0 }, + + /* glo_str3w — str payload (3-word header); the multi-word slot copy + * over the global base must be complete. */ + { "glo_str3w", + "package main;\n" + "let gs: [2](int | str) = [0, 0];\n" + "export fn main() int = {\n" + " gs[1] = \"hello\";\n" + " match (gs[1]) {\n" + " case int => { return 1; };\n" + " case let s: str => { if (s.len: int != 5) { return 2; }; return 0; };\n" + " };\n" + "};\n", + 0 }, + + /* glo_struct2w — a 16B struct payload (slot = tag + 2 payload words); + * proves the mid-count copy loop moves BOTH payload words. */ + { "glo_struct2w", + "package main;\n" + "type pair = struct { a: i64, b: i64 };\n" + "let gs: [2](int | pair) = [0, 0];\n" + "export fn main() int = {\n" + " gs[1] = pair { a = 0x1111, b = 0x2222 };\n" + " match (gs[1]) {\n" + " case int => { return 1; };\n" + " case let q: pair => {\n" + " if (q.a != 0x1111) { return 2; };\n" + " if (q.b != 0x2222) { return 3; };\n" + " return 0;\n" + " };\n" + " };\n" + "};\n", + 0 }, + + /* loc_ctl — LOCAL tagged array: the frame-base path that already worked + * must stay correct (the fix must not perturb the non-global branch). */ + { "loc_ctl", + "package main;\n" + "export fn main() int = {\n" + " let a: [2](int | bool) = [0, 0];\n" + " a[1] = 42;\n" + " match (a[1]) {\n" + " case let n: int => { if (n != 42) { return 1; }; return 0; };\n" + " case bool => { return 2; };\n" + " };\n" + "};\n", + 0 }, + + /* scalar_ctl — scalar global array: the template arm the fix mirrors. */ + { "scalar_ctl", + "package main;\n" + "let arr: [3]int = [0, 0, 0];\n" + "export fn main() int = {\n" + " arr[1] = 42;\n" + " if (arr[1] != 42) { return 1; };\n" + " return 0;\n" + "};\n", + 0 }, +}; + +/* run_build — build+run `src` via `driver`; returns the binary's exit code, + * or -1 on a build failure. */ +static int +run_build(const char *driver, const struct row *r, int i) +{ + char src[64], tmpdir[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/tagglobidx_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/tagglobidx_%d_d_%d", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -2; + fputs(r->src, f); + fclose(f); + + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>/dev/null", + tmpdir, driver, src); + int brc = runwait(cmd); + + 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 = -1; + if (brc == 0) got = runwait(outbin); + + unlink(src); unlink(outbin); rmdir(tmpdir); + return brc == 0 ? got : -1; +} + +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], wdrv[1024]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + int have_ww = (access(wdrv, X_OK) == 0); + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + + for (int i = 0; i < n; i++) { + total++; + int gc = run_build(cdrv, &rows[i], i); + if (gc < 0) { + fprintf(stderr, "taggedglobalindex_run[cstage][%s]: build/run " + "failed (got %d)\n", rows[i].label, gc); + fail++; + continue; + } + if (rows[i].want_exit >= 0 && gc != rows[i].want_exit) { + fprintf(stderr, "taggedglobalindex_run[cstage][%s]: exit=%d " + "want=%d (global base dropped / store lost)\n", + rows[i].label, gc, rows[i].want_exit); + fail++; + } + if (!have_ww) { + fprintf(stderr, "taggedglobalindex_run: skip wwstage (no %s)\n", + wdrv); + continue; + } + int gw = run_build(wdrv, &rows[i], i); + if (gw != gc) { + fprintf(stderr, "taggedglobalindex_run[%s]: cs=%d != ww=%d " + "(#16 indexed-global-tagged base divergence)\n", + rows[i].label, gc, gw); + fail++; + } + if (rows[i].want_exit >= 0 && gw != rows[i].want_exit) { + fprintf(stderr, "taggedglobalindex_run[wwstage][%s]: exit=%d " + "want=%d (global base dropped / store lost)\n", + rows[i].label, gw, rows[i].want_exit); + fail++; + } + } + + if (fail) { + fprintf(stderr, "taggedglobalindex_run: %d/%d checks failed\n", + fail, total); + return 1; + } + printf("taggedglobalindex_run: %d/%d ok\n", total, total); + return 0; +}