/* * 989_tupfieldsize_run — F7-c4 (#43): a for-range destructure over an * array of tuples must stride by the tuple's TRUE size; a slice/str/nested * tuple FIELD carries its full width, not the 8B scalar default. * * THE BUG (cat-A silent miscompile, gate-blind): paramfieldsize * (selfhost/cmd/wcc/cgenstmt.ww) — the structural sizer the for-range * destructure uses to compute the tuple stride and each field's offset — * had no N_TSLICE / N_TTUPLE arm, so a `[]T` tuple-field sized 8 (the * default) instead of its 24B header. The #270-1c array-literal guard * blocks only the literal CONSTRUCTION; the for-range DESTRUCTURE-READ * path is unguarded (ken's oracle refuted "unreachable"). For * `[2]([]u8, i64)` the wwstage strode the tuple at 16 / read field-2 at * offset 8, vs cstage's 32 / 24 — a cs≠ww divergence (the cat-A * signature). cstage's tp->type->size (cmd/w6c/cgen.c N_FORRANGE) reads * the true width. THE FIX: add the N_TSLICE arm (tyslicesize() = 24, the * slice-header SSoT, rule-13) and the N_TTUPLE arm (sum of 8B-floored * element slots, recursive), aligning wwstage UP. * * #40 (#263, now CLOSED both stages — fused): F7-c4 fixed only the STRIDE, * so the slice-field row was pinned cs==ww (MATCH-ONLY) at 40, NOT the * arithmetic-expected 45 — because the destructure LOAD-into-binding copied * only the PTR word of the 24B slice binding, dropping .len/.cap (both stages * identically; nobody had re-measured the ww absolute after F7-c4). The fused * #40 fix (cgen.c N_FORRANGE destructure + cgenstmt.ww cgforrange twin: copy * the binding's FULL extent for an aggregate sz>8 binding, same word-run + * sized-tail idiom as the non-destructure copy) lands all three header words. * The rows now pin the ABSOLUTE value cs==ww across all three words — the * recurring dropped word in this 24B-aggregate-destructure family is the CAP * (cf #29 chained-dot, #43 sizer), so the cap row reads b.cap with cap!=len * (sliced) to teeth it; a ptr+len-only set would pass green while cap stayed * stale (exactly how #40 hid). * * Tuples are built by whole-tuple element store (`xs[i] = (..)`). * * Rows (cstage `ww` + wwstage `ww_ww`; rule-10; all pin the absolute value): * row | reads | exit (cs==ww) * ------------------+------------------------------------+-------------- * len_read | len(b)+n over [2]([]u8,i64) | 45 (2+10+3+30) * cap_read | b.cap+n, sliced caps 4/5 (cap!=len) | 49 (4+10+5+30) * ptr_read | b[0]+ (byte through ptr) | 217 (97+120) * scalar_tuple_ctl | for(.. [2](i64,i64)) | 48 (control) * Pre-fix (single-word copy): len_read=40, cap_read=40 (len/cap words both 0), * ptr_read=217 (word0 always copied — coverage, no teeth); cs==ww==wrong (the * #263 both-wrong-identical residual the fused fix retires). */ #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: also pin the absolute value; -1: cs==ww only */ }; static const struct row rows[] = { /* (1) len_read (#40 teeth, word 1): the slice binding's .len must be * copied. Pre-fix the single-word copy dropped it → len(b)=0 → 40. */ { "len_read", "package main;\n" "export fn main() int = {\n" " let b0: []u8 = ['a', 'b'];\n" " let b1: []u8 = ['x', 'y', 'z'];\n" " let xs: [2]([]u8, i64);\n" " xs[0] = (b0, 10);\n" " xs[1] = (b1, 30);\n" " let sum: i64 = 0;\n" " for (let (b, n) .. xs) {\n" " sum = sum + len(b): i64 + n;\n" " };\n" " return sum: int;\n" "};\n", 45 }, /* (2) cap_read (#40 teeth, word 2 — THE recurring dropped word): caps * 4/5 are SLICED so cap != len (2/3); pre-fix b.cap=0 → 40. */ { "cap_read", "package main;\n" "export fn main() int = {\n" " let base0: []u8 = ['a', 'b', 'c', 'd'];\n" " let base1: []u8 = ['p', 'q', 'r', 's', 't'];\n" " let xs: [2]([]u8, i64);\n" " xs[0] = (base0[0:2], 10);\n" " xs[1] = (base1[0:3], 30);\n" " let sum: i64 = 0;\n" " for (let (b, n) .. xs) {\n" " sum = sum + b.cap: i64 + n;\n" " };\n" " return sum: int;\n" "};\n", 49 }, /* (3) ptr_read (word 0 — coverage; word0 was always copied, no teeth): * a byte read through the binding's .ptr. b0[0]='a'=97, b1[0]='x'=120. */ { "ptr_read", "package main;\n" "export fn main() int = {\n" " let b0: []u8 = ['a', 'b'];\n" " let b1: []u8 = ['x', 'y', 'z'];\n" " let xs: [2]([]u8, i64);\n" " xs[0] = (b0, 10);\n" " xs[1] = (b1, 30);\n" " let sum: i64 = 0;\n" " for (let (b, n) .. xs) { sum = sum + b[0]: i64; };\n" " return sum: int;\n" "};\n", 217 }, /* (4) control — scalar-only tuple (no aggregate field): paramfieldsize * already handled it; the sz>8 arm must not touch it. 3+10+5+30 == 48. */ { "scalar_tuple_ctl", "package main;\n" "export fn main() int = {\n" " let xs: [2](i64, i64);\n" " xs[0] = (3, 10); xs[1] = (5, 30);\n" " let sum: i64 = 0;\n" " for (let (a, b) .. xs) { sum = sum + a + b; };\n" " return sum: int;\n" "};\n", 48 }, }; /* 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/tupfs_%d_%d.ww", getpid(), i); snprintf(tmpdir, sizeof tmpdir, "/tmp/tupfs_%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); /* cstage must build+run */ if (gc < 0) { fprintf(stderr, "tupfieldsize_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, "tupfieldsize_run[cstage][%s]: exit=%d " "want=%d\n", rows[i].label, gc, rows[i].want_exit); fail++; } if (!have_ww) { fprintf(stderr, "tupfieldsize_run: skip wwstage (no %s)\n", wdrv); continue; } int gw = run_build(wdrv, &rows[i], i); /* rule-10: the cat-A invariant is cs == ww */ if (gw != gc) { fprintf(stderr, "tupfieldsize_run[%s]: cs=%d != ww=%d " "(stride/offset divergence — #43)\n", rows[i].label, gc, gw); fail++; } if (rows[i].want_exit >= 0 && gw != rows[i].want_exit) { fprintf(stderr, "tupfieldsize_run[wwstage][%s]: exit=%d " "want=%d\n", rows[i].label, gw, rows[i].want_exit); fail++; } } if (fail) { fprintf(stderr, "tupfieldsize_run: %d/%d checks failed\n", fail, total); return 1; } printf("tupfieldsize_run: %d/%d ok\n", total, total); return 0; }