diff --git a/Makefile b/Makefile index 80b47043..fa509fae 100644 --- a/Makefile +++ b/Makefile @@ -257,6 +257,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_str_elem_cap_run \ $(BIN)/test_str_field_cap_run \ $(BIN)/test_str_chained_field_cap_run \ + $(BIN)/test_str_tuple_elem_cap_run \ $(BIN)/test_composite_call_arg \ $(BIN)/test_composite_call_arg_run \ $(BIN)/test_letdecl_zeroinit \ @@ -641,6 +642,12 @@ $(BIN)/test_str_chained_field_cap_run: test/wcc/934_str_chained_field_cap_run.c $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_str_tuple_elem_cap_run: test/wcc/935_str_tuple_elem_cap_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 8c406c32..f7a6a0ad 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -5834,13 +5834,18 @@ cgexpr(Cg *c, Node *n, Local *locals) ? tp->type->under : tp->type; int op = fldloadop(tp->type, fsz); int off = localfind(locals, n->lhs->str); - /* str element: load (ptr, len) into (AX, BX) so chains - * like `t.1.len` propagate through the str-rhs - * convention. Without this we'd MOVQ 8B and the .len - * shuffle (BX→AX) would surface garbage. */ + /* str IS []u8 — load (ptr, len, cap) into + * (AX, BX, CX), the canonical slice-header ABI, + * so chains like `t.1.len` propagate through the + * slice-rhs convention (#1/Phase 3 collapse). + * UNLIKE the field arms there is no slice-element + * sibling here, so the triple is hand-authored; + * base is BP (frame, not a target reg) so the + * canonical ptr/len/cap order has no clobber risk. */ if (fu && fu->kind == TY_STR) { ins2(c, A_MOVQ, amem(D_BP, off + foff + 0), areg(D_AX)); ins2(c, A_MOVQ, amem(D_BP, off + foff + 8), areg(D_BX)); + ins2(c, A_MOVQ, amem(D_BP, off + foff + 16), areg(D_CX)); break; } ins2(c, op, amem(D_BP, off + foff), areg(D_AX)); diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 31f1f79f..777eb36f 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -15336,9 +15336,11 @@ fn cgdot(c: *cgen, n: *node) void = { // Walk the tuple element type list summing slotsize // (matches the (scalar, str) init layout: scalar in an // 8B slot, str in 24B — str IS []u8, #1/Phase 3). For a - // str element, load (ptr, len) into (AX, BX); the cap - // stays in the slot (the 2-word str-field read, like - // every other chained/dot str leaf read — task #14). + // str element, load (ptr, len, cap) into (AX, BX, CX), + // the canonical slice-header ABI. No slice-element + // sibling here, so the triple is hand-authored; base is + // BP (frame, not a target reg) so ptr/len/cap order has + // no clobber risk. if (lkind == nkind.N_TTUPLE) { let idx: i32 = fldnumidx(fld); if (idx >= 0) { @@ -15362,6 +15364,9 @@ fn cgdot(c: *cgen, n: *node) void = { emitline("\tMOVQ\t"); emitoff((lc.off + foff + 8): i64); emitline("(BP), BX\n"); + emitline("\tMOVQ\t"); + emitoff((lc.off + foff + 16): i64); + emitline("(BP), CX\n"); return; }; let sz: i32 = slotsize(c, tpt); diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index de6abd12..2a11c9c4 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -1508,9 +1508,11 @@ fn cgdot(c: *cgen, n: *node) void = { // Walk the tuple element type list summing slotsize // (matches the (scalar, str) init layout: scalar in an // 8B slot, str in 24B — str IS []u8, #1/Phase 3). For a - // str element, load (ptr, len) into (AX, BX); the cap - // stays in the slot (the 2-word str-field read, like - // every other chained/dot str leaf read — task #14). + // str element, load (ptr, len, cap) into (AX, BX, CX), + // the canonical slice-header ABI. No slice-element + // sibling here, so the triple is hand-authored; base is + // BP (frame, not a target reg) so ptr/len/cap order has + // no clobber risk. if (lkind == nkind.N_TTUPLE) { let idx: i32 = fldnumidx(fld); if (idx >= 0) { @@ -1534,6 +1536,9 @@ fn cgdot(c: *cgen, n: *node) void = { emitline("\tMOVQ\t"); emitoff((lc.off + foff + 8): i64); emitline("(BP), BX\n"); + emitline("\tMOVQ\t"); + emitoff((lc.off + foff + 16): i64); + emitline("(BP), CX\n"); return; }; let sz: i32 = slotsize(c, tpt); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index bb80071c..9575c45c 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -15336,9 +15336,11 @@ fn cgdot(c: *cgen, n: *node) void = { // Walk the tuple element type list summing slotsize // (matches the (scalar, str) init layout: scalar in an // 8B slot, str in 24B — str IS []u8, #1/Phase 3). For a - // str element, load (ptr, len) into (AX, BX); the cap - // stays in the slot (the 2-word str-field read, like - // every other chained/dot str leaf read — task #14). + // str element, load (ptr, len, cap) into (AX, BX, CX), + // the canonical slice-header ABI. No slice-element + // sibling here, so the triple is hand-authored; base is + // BP (frame, not a target reg) so ptr/len/cap order has + // no clobber risk. if (lkind == nkind.N_TTUPLE) { let idx: i32 = fldnumidx(fld); if (idx >= 0) { @@ -15362,6 +15364,9 @@ fn cgdot(c: *cgen, n: *node) void = { emitline("\tMOVQ\t"); emitoff((lc.off + foff + 8): i64); emitline("(BP), BX\n"); + emitline("\tMOVQ\t"); + emitoff((lc.off + foff + 16): i64); + emitline("(BP), CX\n"); return; }; let sz: i32 = slotsize(c, tpt); diff --git a/test/wcc/935_str_tuple_elem_cap_run.c b/test/wcc/935_str_tuple_elem_cap_run.c new file mode 100644 index 00000000..ba8bf229 --- /dev/null +++ b/test/wcc/935_str_tuple_elem_cap_run.c @@ -0,0 +1,169 @@ +/* + * 935_str_tuple_elem_cap_run — runtime coverage for the C4.6-S3 fold: a + * tuple POSITIONAL str element read `t.N` (N_IDENT base, TY_TUPLE) must load + * the full 24B {ptr,len,cap} header, not just {ptr,len}. str is 24B since + * Phase 2 (#1); pre-S3 the tuple str-element arm loaded 2 words (ptr in AX, + * len in BX) and dropped cap. + * + * This is the "author-to-ABI" coda of C4.6: UNLIKE S1/S2/caseB there is no + * adjacent slice-element arm to fold onto, so the 3-word triple is authored + * directly to the canonical slice-header ABI (AX=ptr, BX=len, CX=cap). The + * base is BP (the frame, not a target reg), so the canonical ptr/len/cap + * order has no clobber subtlety. Sites: cgen.c's "tuple positional field + * access" branch and the cgenexpr.ww N_TTUPLE twin. + * + * The byte-id gates (990-997) can't catch a symmetric 2-word miscompile: if + * both stages drop cap identically, byte-id passes silently. So this pins the + * *runtime* contract — build through both the cstage `ww` and wwstage `ww_ww` + * driver and confirm the assertion holds (exit 0). + * + * The tuple is built by a (i64, str)-returning mk() so the poisoned cap=8 + * rides the 4-word return ABI (AX=scalar, DX=ptr, CX=len, R8=cap) into the + * tuple slot's +24 word; `t.1` then reads it back 3-word (cap at slot+8+16). + * + * DISCRIMINATION (heed the 933/934 lesson): a 2-word read leaves CX untouched, + * so the row could coincidentally pass on a stale CX. spoil() interposes a + * call between the `let t = mk()` build and the `t.1` read; a call clobbers + * caller-saved CX (spoil's own str copy leaves cap=44 in CX), so a broken + * 2-word read observes cap=44, not 8, and the row fails (return 1). Verified + * fail-before (reverted +16 cap load: exit 1) / pass-after (exit 0), both + * drivers. + */ +#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[] = { + /* S3 — `t.N` positional str element. t a local (i64, str) tuple + * built by mk() so the poison cap=8 (len=2) rides the return ABI + * into the str element's slot. spoil() interposes a CX-clobbering + * call between the build and the `t.1` read so a broken 2-word read + * cannot coincidentally pass on a stale CX. */ + { "tuple_positional_str_elem", + "fn mk() (i64, str) = {\n" + " let p: str = \"hi\";\n" + " p.cap = 8i32;\n" + " return (5i64, p);\n" + "};\n" + "fn spoil() i32 = {\n" + " let z: str = \"zzzz\";\n" + " z.cap = 44i32;\n" + " let w: str = z;\n" + " return w.cap: i32;\n" + "};\n" + "export fn main() i32 = {\n" + " let t: (i64, str) = mk();\n" + " let junk: i32 = spoil();\n" + " let s: str = t.1;\n" + " if (s.cap: i32 != 8) { return 1; };\n" + " if (s.len: i32 != 2) { return 2; };\n" + " if (junk != 44) { 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/strtupleelemcap_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/strtupleelemcap_%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, + "str_tuple_elem_cap_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, + "str_tuple_elem_cap_run[%s][%s]: exit=%d want=%d\n", + drivers[d].name, rows[i].label, + got, rows[i].want); + fail++; + } + } + } + + if (fail) { + fprintf(stderr, "str_tuple_elem_cap_run: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("str_tuple_elem_cap_run: %d/%d ok\n", total, total); + return 0; +}