From 1304db8871ac729909516d04ae1f836656876da8 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 25 May 2026 02:59:40 +0900 Subject: [PATCH] cgen: *p=sliceval deref store -> 3-word (both stages, #79) A slice VALUE stored through a whole-deref lhs `*p = v` dropped len+cap: the `*p = v` arm kind-gated its 3-word {ptr,len,cap} stash+store on str ONLY, so a slice fell to the 1-word fldstoreop default (ptr only). The deref READ is 3-word, so the reader got garbage len/cap -- correctness, not perf. str IS []u8 since #1, so the str machinery applies verbatim; widen the gate str -> str||slice (kind-OR, not a sz==24 test). This is the project #75 str-only-gate one level down (deref-store). cstage cmd/w6c/cgen.c:3792/3800 (two gates); wwstage cgenexpr.ww `*p=v` twin detects N_TSLICE syntactically (mirror str). Both stages dropped identically, so cs==ww + 990-997 + byte-id are all gate-blind here -- only a store->read roundtrip catches it. New 944_deref_slice_store_run asserts the {ptr,len,cap} survives a poisoned dst, via the direct local and the field-deref read; str-deref + (*p).field controls guard the untouched arms. Verified fail-before (1-word ptr store) / pass-after (8/8), byte-identical asm both stages. Out-of-gate, deferred to #80: the wwstage syntactic detection is alias-BLIND -- a slice-alias `*Foo` (Foo=[]T) or non-ident deref-store stays 1-word, the SAME retained divergence str already carries (cstage's resolved-type vt fires in both). #80 unifies detection by aligning the wwstage UP, not gating cstage down. Separately surfaced (filed apart, not touched here): the whole-deref READ-into-let `let v = *p` drops len+cap for a slice while the str form is 3-word -- the read-side twin of this store hole. --- Makefile | 7 + cmd/w6c/cgen.c | 12 +- selfhost/cmd/w6c/main.combined.ww | 10 ++ selfhost/cmd/wcc/cgenexpr.ww | 10 ++ selfhost/cmd/wwdump/main.combined.ww | 10 ++ test/wcc/944_deref_slice_store_run.c | 215 +++++++++++++++++++++++++++ 6 files changed, 259 insertions(+), 5 deletions(-) create mode 100644 test/wcc/944_deref_slice_store_run.c diff --git a/Makefile b/Makefile index 03dafc89..d75762a3 100644 --- a/Makefile +++ b/Makefile @@ -265,6 +265,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_slice_store_cap_run \ $(BIN)/test_subslice_cap_run \ $(BIN)/test_subslice_ptresz_run \ + $(BIN)/test_deref_slice_store_run \ $(BIN)/test_str_forrange_loopvar_run \ $(BIN)/test_composite_call_arg \ $(BIN)/test_composite_call_arg_run \ @@ -704,6 +705,12 @@ $(BIN)/test_subslice_ptresz_run: test/wcc/943_subslice_ptresz_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_deref_slice_store_run: test/wcc/944_deref_slice_store_run.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_composite_call_arg: test/wcc/723_composite_call_arg.c \ $(BIN)/w6c $(BIN)/w6c_ww | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 8c885324..a7184f48 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -3787,17 +3787,19 @@ cgexpr(Cg *c, Node *n, Local *locals) ins2(c, mov, areg(D_X0), amem(D_BX, 0)); break; } - cgexpr(c, n->rhs, locals); /* AX=ptr (BX=len, CX=cap if str) */ + cgexpr(c, n->rhs, locals); /* AX=ptr (BX=len, CX=cap if str/slice) */ ins1(c, A_PUSHQ, areg(D_AX)); - if (vt && vt->kind == TY_STR) { - /* str IS []u8: also stash len + cap across the - * pointer eval, which clobbers BX/CX (#1/Phase 3). */ + if (vt && (vt->kind == TY_STR || vt->kind == TY_SLICE)) { + /* str IS []u8 and a slice is the same 3-word + * {ptr,len,cap} header (ref/hare/rt/ensure.ha:4-8): + * stash len + cap across the pointer eval, which + * clobbers BX/CX (#1/Phase 3; slice arm #79). */ ins1(c, A_PUSHQ, areg(D_BX)); /* len */ ins1(c, A_PUSHQ, areg(D_CX)); /* cap */ } cgexpr(c, n->lhs->lhs, locals); /* AX = pointer */ ins2(c, A_MOVQ, areg(D_AX), areg(D_BX)); - if (vt && vt->kind == TY_STR) { + if (vt && (vt->kind == TY_STR || vt->kind == TY_SLICE)) { ins1(c, A_POPQ, areg(D_CX)); /* cap */ ins2(c, A_MOVQ, areg(D_CX), amem(D_BX, 16)); ins1(c, A_POPQ, areg(D_CX)); /* len */ diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 35c5a7a0..e51f730d 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -17559,6 +17559,16 @@ fn cgassign(c: *cgen, n: *node) void = { else { if (ps == 4) { storeop = "MOVL"; }; }; }; }; }; }; + // A slice IS the same 3-word {ptr,len,cap} + // header as str (ref/hare/rt/ensure.ha:4-8), + // so `*p = sliceval` takes str's stash+store + // path (#79; precedent cgenstmt.ww:1631, + // cgenexpr.ww:1684). LIKE str this is + // alias-BLIND: a slice-alias `*Foo` / non-ident + // deref-store stays 1-word, the SAME divergence + // str carries; resolved-vs-syntactic detection + // is unified UP in #80, not patched here. + if (pe.kind == nkind.N_TSLICE) { elemstr = true; }; }; }; }; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 660977dc..3b15759c 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -3700,6 +3700,16 @@ fn cgassign(c: *cgen, n: *node) void = { else { if (ps == 4) { storeop = "MOVL"; }; }; }; }; }; }; + // A slice IS the same 3-word {ptr,len,cap} + // header as str (ref/hare/rt/ensure.ha:4-8), + // so `*p = sliceval` takes str's stash+store + // path (#79; precedent cgenstmt.ww:1631, + // cgenexpr.ww:1684). LIKE str this is + // alias-BLIND: a slice-alias `*Foo` / non-ident + // deref-store stays 1-word, the SAME divergence + // str carries; resolved-vs-syntactic detection + // is unified UP in #80, not patched here. + if (pe.kind == nkind.N_TSLICE) { elemstr = true; }; }; }; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 0b53811e..67c81b0e 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -17559,6 +17559,16 @@ fn cgassign(c: *cgen, n: *node) void = { else { if (ps == 4) { storeop = "MOVL"; }; }; }; }; }; }; + // A slice IS the same 3-word {ptr,len,cap} + // header as str (ref/hare/rt/ensure.ha:4-8), + // so `*p = sliceval` takes str's stash+store + // path (#79; precedent cgenstmt.ww:1631, + // cgenexpr.ww:1684). LIKE str this is + // alias-BLIND: a slice-alias `*Foo` / non-ident + // deref-store stays 1-word, the SAME divergence + // str carries; resolved-vs-syntactic detection + // is unified UP in #80, not patched here. + if (pe.kind == nkind.N_TSLICE) { elemstr = true; }; }; }; }; diff --git a/test/wcc/944_deref_slice_store_run.c b/test/wcc/944_deref_slice_store_run.c new file mode 100644 index 00000000..5d450020 --- /dev/null +++ b/test/wcc/944_deref_slice_store_run.c @@ -0,0 +1,215 @@ +/* + * 944_deref_slice_store_run — runtime coverage for project #79: a `[]T` + * (slice-typed) VALUE stored through a WHOLE-deref lhs `*p = sliceval` must + * move the full 24B {ptr,len,cap} header (ref/hare/rt/ensure.ha:4-8), not + * just {ptr}. The `*p = v` deref-store arm was kind-gated on str ONLY; a + * slice fell to the 1-word fldstoreop default and silently DROPPED len+cap. + * str IS []u8 since Phase 2 (#1), so the str 3-word stash+store machinery + * applies to slices verbatim — the fix widens the gate from `str` to + * `str || slice` (kind-OR, never a sz==24 test, which would also catch + * >16B structs). This is the project #75 str-only-gate one level down + * (deref-store). + * + * SYMMETRIC across both stages (cstage cmd/w6c/cgen.c:3792/3800 + wwstage + * cgenexpr.ww `*p=v` twin both dropped identically), so byte-id + cs==ww + + * 990-997 are all gate-BLIND here — only a store->read ROUNDTRIP catches it. + * + * Rows (each store paired with a read mirror; cap!=len in every row so a + * dropped len OR cap is caught): + * A d_slice_direct — `*pp = p`, read back via the DIRECT local `dst`. + * dst is POISONED first with a different slice q (len=4,cap=5) via a + * PROVEN 3-word store (`dst = q`, the whole-local slice assign, NOT the + * site under test); a 1-word `*pp=p` leaves q's cap=5, the fix lands 8. + * B d_slice_thru — same store, read back THROUGH the pointer via the + * field-deref read `(*pp).cap`/`(*pp).len` (already 3-word), confirming + * the stored header survives a pointer-side read. + * C control str-deref — `*pp = "hello"` over a poisoned `d="xy"`; the str + * arm we widen AROUND, must still land len=5 (regression guard). + * D control (*p).field — `(*pb).s = p` into a struct{s:[]u8} field; a + * SEPARATE branch (explicit-deref N_DOT), already 3-word, untouched. + * + * The whole-deref READ-into-let `let v = *pp` is deliberately NOT used: for + * a slice it drops len+cap (ptr-only) while the str form is 3-word — a + * SEPARATE read-side gate hole, filed apart from this store fold (project + * #81, the read-twin of #79). The element-index read `(*pp)[i]` is likewise + * avoided (index-through-deref-field diverges cs vs ww — also separate, + * project #82); ptr is covered by row A's dst[0]. + * + * Verified fail-before (cstage store emits 1-word ptr-only, row A reads the + * poison cap=5 -> exit 1) / pass-after (3-word store, exit 0) on BOTH the + * cstage `ww` and wwstage `ww_ww` drivers, byte-identical asm. + * NNN<950, self-contained (/tmp, no imports), so rule-14's selfhost-sibling + * race does not apply (mirrors the 941/942/943 precedent). + */ +#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[] = { + /* A — d_slice_direct: poison dst via the proven 3-word `dst = q`, + * then `*pp = p` and read the direct local. p:{cap=8,len=2,[0]=104}, + * q:{cap=5,len=4}. A dropped store keeps q's cap=5. */ + { "d_slice_direct", + "export fn main() i32 = {\n" + " let hb: [8]u8; hb[0] = 104u8;\n" + " let p: []u8; p.ptr = &hb[0]; p.len = 2; p.cap = 8;\n" + " let qb: [8]u8;\n" + " let q: []u8; q.ptr = &qb[0]; q.len = 4; q.cap = 5;\n" + " let dst: []u8 = q;\n" + " let pp: *[]u8 = &dst;\n" + " *pp = p;\n" + " if (dst.cap: i32 != 8) { return 1; };\n" + " if (dst.len: i32 != 2) { return 2; };\n" + " if (dst[0] != 104u8) { return 3; };\n" + " return 0;\n" + "};\n", + 0 }, + /* B — d_slice_thru: same store, read back THROUGH the pointer via the + * 3-word field-deref read `(*pp).cap`/`.len`. */ + { "d_slice_thru", + "export fn main() i32 = {\n" + " let hb: [8]u8; hb[0] = 104u8;\n" + " let p: []u8; p.ptr = &hb[0]; p.len = 2; p.cap = 8;\n" + " let qb: [8]u8;\n" + " let q: []u8; q.ptr = &qb[0]; q.len = 4; q.cap = 5;\n" + " let dst: []u8 = q;\n" + " let pp: *[]u8 = &dst;\n" + " *pp = p;\n" + " if ((*pp).cap: i32 != 8) { return 1; };\n" + " if ((*pp).len: i32 != 2) { return 2; };\n" + " return 0;\n" + "};\n", + 0 }, + /* C — control str-deref: the str arm the fix widens around. Poison + * d="xy" then `*pp="hello"`; must still land len=5. */ + { "d_str_control", + "export fn main() i32 = {\n" + " let d: str = \"xy\";\n" + " let pp: *str = &d;\n" + " *pp = \"hello\";\n" + " if (d.len: i32 != 5) { return 1; };\n" + " return 0;\n" + "};\n", + 0 }, + /* D — control (*p).field: explicit-deref field store, a separate + * already-3-word branch. `(*pb).s = p` into a struct{s:[]u8}. */ + { "d_field_control", + "type box = struct { s: []u8 };\n" + "export fn main() i32 = {\n" + " let hb: [8]u8; hb[0] = 104u8;\n" + " let p: []u8; p.ptr = &hb[0]; p.len = 2; p.cap = 8;\n" + " let b: box;\n" + " let pb: *box = &b;\n" + " (*pb).s = p;\n" + " if (b.s.cap: i32 != 8) { return 1; };\n" + " if (b.s.len: i32 != 2) { return 2; };\n" + " if (b.s[0] != 104u8) { return 3; };\n" + " return 0;\n" + "};\n", + 0 }, +}; + +static int +run_driver(const char *driver, const struct row *r, int i) +{ + char src[96], tmpdir[96], cmd[1024]; + snprintf(src, sizeof src, "/tmp/derefslicestore_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/derefslicestore_%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[160]; + 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[512]; + if (bin[0] != '/') { + char cwd[256]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char cdrv[640]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + char wdrv[640]; + 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, + "deref_slice_store_run: 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, + "deref_slice_store_run[%s][%s]: exit=%d want=%d\n", + drivers[d].name, rows[i].label, + got, rows[i].want); + fail++; + } + } + } + + if (fail) { + fprintf(stderr, "deref_slice_store_run: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("deref_slice_store_run: %d/%d ok\n", total, total); + return 0; +}