diff --git a/Makefile b/Makefile index b9e8f642..6ea01c90 100644 --- a/Makefile +++ b/Makefile @@ -273,7 +273,6 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_tagtupfieldsize_run \ $(BIN)/test_nestfield_run \ $(BIN)/test_structlocal_frame \ - $(BIN)/test_structcopytail_run \ $(BIN)/test_arrlit_tail_zero_run \ $(BIN)/test_allocalias_run \ $(BIN)/test_slttypepref_run \ @@ -898,22 +897,6 @@ $(BIN)/test_structlocal_frame: test/wcc/989_structlocal_frame.c \ $(BIN)/w6c $(BIN)/w6c_ww | $(BIN) $(CC) $(CFLAGS) -o $@ $< -# 989_structcopytail_run (#73): a whole-struct field-copy `x.i = o` whose -# source natural size %8 lands in {2,3,5,6,7} must move exactly that many -# bytes via a descending greedy 8->4->2->1 tail (MOVL/MOVW/MOVB), not round -# the ragged remainder up to an 8-byte MOVQ that over-writes the field's -# natural-offset successor. BOTH stages inlined a tail handling only {4,1}; -# tail-2 (inner2{u8,u8} in outer2{i,mark:i32}) and tail-6 (inner6{u16,u16, -# u16} in outer6{i,mark:u8}) over-wrote MARK with an 8B MOVQ. Builds+runs on -# BOTH driver twins (rule-10), pinning the absolute 0; ctl8 (8-aligned, zero -# tail) pins the unchanged path. -$(BIN)/test_structcopytail_run: test/wcc/989_structcopytail_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_taggedderefstore_run (#17): a deref-target store `*p = v` (p:*tagged) # sized the write off the pointee and emitted ONE fldstoreop — rhs landed in # the tag word, payload dropped, the union discriminant corrupted. BOTH-stage- @@ -3429,7 +3412,7 @@ test-lang: all LANGBYTEID_DIR = $(OUT)/langbyteid LANGBYTEID_FILES = $(wildcard test/lang/*_test.ww) LANGBYTEID_VERB = test -c -LANGBYTEID_EXPECTED_MIN = 34 +LANGBYTEID_EXPECTED_MIN = 35 $(if $(LANGBYTEID_FILES),,$(error test-lang-byteid: empty corpus)) test-lang-byteid: all @set -e; \ diff --git a/test/lang/structcopytail_test.ww b/test/lang/structcopytail_test.ww new file mode 100644 index 00000000..dfcd237b --- /dev/null +++ b/test/lang/structcopytail_test.ww @@ -0,0 +1,76 @@ +// structcopytail_test — whole-struct field-copy ragged tail (#73, #263 both +// stages), migrated from test/wcc/989_structcopytail_run.c. A `x.i = o` copy of +// a struct whose NATURAL size is not 8-aligned must move exactly that many bytes +// via a descending-greedy 8->4->2->1 tail (MOVL/MOVW/MOVB), NOT round up to an +// 8-byte MOVQ that bleeds into the field's natural-offset successor. Pre-#73 +// both stages' inline tail handled only {4,1}; a natural size %8 in {2,3,5,6,7} +// fell to a single MOVQ writing [0,8) and clobbering the `mark` field placed at +// the inner struct's natural offset. The fix routes both stages' field copy +// through the canonical greedy emitter. Each row poisons `mark` first, copies, +// then reads every field back; the .c returned the 1-based index of the first +// mismatch (0 = all-correct), here each field is asserted directly. T2 +// (test-lang-byteid) keeps the cs==ww net the .c twin's dual-driver run gave. + +package structcopytail_test; + +type inner2 = struct { a: u8, b: u8 }; +type outer2 = struct { i: inner2, mark: i32 }; + +type inner6 = struct { a: u16, b: u16, c: u16 }; +type outer6 = struct { i: inner6, mark: u8 }; + +type inner7 = struct { a: u8, b: u8, c: u8, d: u8, e: u8, f: u8, g: u8 }; +type outer7 = struct { i: inner7, mark: u8 }; + +type inner8 = struct { a: i64 }; +type outer8 = struct { i: inner8, mark: i64 }; + +@test fn tail2() void = { + // 2-byte inner; mark (nat offset 4) must survive the MOVW tail. + let o: inner2 = inner2 { a = 0x11, b = 0x22 }; + let x: outer2; + x.mark = 0x7f7f7f7f; + x.i = o; + assert(x.i.a: int == 0x11); + assert(x.i.b: int == 0x22); + assert(x.mark == 0x7f7f7f7f); +}; + +@test fn tail6() void = { + // 6-byte inner; mark:u8 (nat offset 6) must survive the MOVL+MOVW tail. + let o: inner6 = inner6 { a = 0x1111, b = 0x2222, c = 0x3333 }; + let x: outer6; + x.mark = 0x5a; + x.i = o; + assert(x.i.a: int == 0x1111); + assert(x.i.b: int == 0x2222); + assert(x.i.c: int == 0x3333); + assert(x.mark: int == 0x5a); +}; + +@test fn tail7() void = { + // 7-byte inner; the full MOVL+MOVW+MOVB ladder; mark:u8 at nat offset 7. + let o: inner7 = inner7 { a = 0x11, b = 0x22, c = 0x33, d = 0x44, + e = 0x55, f = 0x66, g = 0x77 }; + let x: outer7; + x.mark = 0x5a; + x.i = o; + assert(x.i.a: int == 0x11); + assert(x.i.b: int == 0x22); + assert(x.i.c: int == 0x33); + assert(x.i.d: int == 0x44); + assert(x.i.e: int == 0x55); + assert(x.i.f: int == 0x66); + assert(x.i.g: int == 0x77); + assert(x.mark: int == 0x5a); +}; + +@test fn ctl8() void = { + // 8-aligned inner; the MOVQ loop alone, zero tail iterations (unchanged). + let o: inner8 = inner8 { a = 0x1234567 }; + let x: outer8; + x.mark = 0x76543210; + x.i = o; + assert(x.i.a == 0x1234567); + assert(x.mark == 0x76543210); +}; diff --git a/test/wcc/989_structcopytail_run.c b/test/wcc/989_structcopytail_run.c deleted file mode 100644 index fc07cb32..00000000 --- a/test/wcc/989_structcopytail_run.c +++ /dev/null @@ -1,235 +0,0 @@ -/* - * 989_structcopytail_run — #73 teeth (recorded by impl-55, 2026-06-13). - * - * Whole-struct field-copy ragged tail. A `x.i = o` copy of a struct whose - * NATURAL size is not 8-aligned must move exactly that many bytes via a - * descending-greedy 8->4->2->1 tail (MOVL/MOVW/MOVB), NOT round up to an - * 8-byte MOVQ that bleeds into the field's natural-offset successor. - * - * THIS TEST IS cs!=ww-RED UNTIL #73. As impl-55 proved (2026-06-13), BOTH - * stages inline a tail handling only {4,1}; a natural size %8 in - * {2,3,5,6,7} falls through to an 8-byte MOVQ over-write: - * - wwstage: cgenexpr.ww 4 field-copy sites (now length-correct via #71, - * tail still 4/1). - * - cstage: cmd/w6c/cgen.c:5302/5318 (+ siblings 2970/3332/5268/6255/ - * 6314/7259) — str_fu->size length is already natural, tail is 4/1. - * ASM PROOF (tail2, x.i = o): cstage emits `MOVQ -8(BP),AX; MOVQ AX,-16(BP)` - * (8B over-write -> clobbers mark@-12). #73 routes both stages' field copy - * through the canonical greedy emitter (cstage cg_aggcopy@2241 / ww - * aggcopy@1662), completing the tail on both -> this test goes green - * cs==ww. Do NOT wire it before #73 lands. - * - * Shapes: - * tail2: inner2{a:u8,b:u8} (size 2) in outer2{i:inner2, mark:i32}; #44 - * packs mark at natural offset 4. A MOVQ copy of i writes [0,8) - * and clobbers mark@4; the MOVW tail writes [0,2). - * tail6: inner6{a:u16,b:u16,c:u16} (size 6) in outer6{i:inner6, mark:u8}; - * mark at natural offset 6. A MOVQ copy writes [0,8) and clobbers - * mark@6; the MOVL+MOVW tail writes [0,6). - * ctl8: inner8{a:i64} (size 8, 8-aligned) — the MOVQ loop alone, no - * tail; proves the 8-aligned path is unchanged. - * Each program reads every field back and returns the 1-based index of the - * first mismatch, 0 on all-correct. Both stages build+run (rule-10), pin 0. - */ -#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[] = { - /* tail2 — whole-copy of a 2-byte struct; mark (nat offset 4) must - * survive. Pre-tail-fix MOVQ writes [0,8) -> mark clobbered. */ - { "tail2", - "package main;\n" - "type inner2 = struct { a: u8, b: u8 };\n" - "type outer2 = struct { i: inner2, mark: i32 };\n" - "export fn main() int = {\n" - " let o: inner2 = inner2 { a = 0x11, b = 0x22 };\n" - " let x: outer2;\n" - " x.mark = 0x7f7f7f7f;\n" - " x.i = o;\n" - " if (x.i.a: int != 0x11) { return 1; };\n" - " if (x.i.b: int != 0x22) { return 2; };\n" - " if (x.mark != 0x7f7f7f7f) { return 3; };\n" - " return 0;\n" - "};\n", - 0 }, - - /* tail6 — whole-copy of a 6-byte struct; mark:u8 (nat offset 6) must - * survive. Pre-tail-fix MOVQ writes [0,8) -> mark clobbered; the - * MOVL+MOVW tail writes exactly [0,6). */ - { "tail6", - "package main;\n" - "type inner6 = struct { a: u16, b: u16, c: u16 };\n" - "type outer6 = struct { i: inner6, mark: u8 };\n" - "export fn main() int = {\n" - " let o: inner6 = inner6 { a = 0x1111, b = 0x2222, c = 0x3333 };\n" - " let x: outer6;\n" - " x.mark = 0x5a;\n" - " x.i = o;\n" - " if (x.i.a: int != 0x1111) { return 1; };\n" - " if (x.i.b: int != 0x2222) { return 2; };\n" - " if (x.i.c: int != 0x3333) { return 3; };\n" - " if (x.mark: int != 0x5a) { return 4; };\n" - " return 0;\n" - "};\n", - 0 }, - - /* tail7 — whole-copy of a 7-byte struct (seven u8, align 1); mark:u8 - * at natural offset 7 must survive. Exercises the FULL descending tail - * ladder in one shape: MOVL[0,4) + MOVW[4,6) + MOVB[6,7). Pre-fix the - * 4/1-only tail defaulted size-7 to a single 8-byte MOVQ writing [0,8) - * -> clobbers mark@7; this is the only row hitting the MOVB tail. */ - { "tail7", - "package main;\n" - "type inner7 = struct { a: u8, b: u8, c: u8, d: u8, e: u8, f: u8, g: u8 };\n" - "type outer7 = struct { i: inner7, mark: u8 };\n" - "export fn main() int = {\n" - " let o: inner7 = inner7 { a = 0x11, b = 0x22, c = 0x33, d = 0x44, e = 0x55, f = 0x66, g = 0x77 };\n" - " let x: outer7;\n" - " x.mark = 0x5a;\n" - " x.i = o;\n" - " if (x.i.a: int != 0x11) { return 1; };\n" - " if (x.i.b: int != 0x22) { return 2; };\n" - " if (x.i.c: int != 0x33) { return 3; };\n" - " if (x.i.d: int != 0x44) { return 4; };\n" - " if (x.i.e: int != 0x55) { return 5; };\n" - " if (x.i.f: int != 0x66) { return 6; };\n" - " if (x.i.g: int != 0x77) { return 7; };\n" - " if (x.mark: int != 0x5a) { return 8; };\n" - " return 0;\n" - "};\n", - 0 }, - - /* ctl8 — 8-aligned struct: the MOVQ loop alone, zero tail iterations. - * Proves the 8-aligned emission path stays correct (byte-id caveat). */ - { "ctl8", - "package main;\n" - "type inner8 = struct { a: i64 };\n" - "type outer8 = struct { i: inner8, mark: i64 };\n" - "export fn main() int = {\n" - " let o: inner8 = inner8 { a = 0x1234567 };\n" - " let x: outer8;\n" - " x.mark = 0x76543210;\n" - " x.i = o;\n" - " if (x.i.a != 0x1234567) { return 1; };\n" - " if (x.mark != 0x76543210) { return 2; };\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/sctail_%d_%d.ww", getpid(), i); - snprintf(tmpdir, sizeof tmpdir, "/tmp/sctail_%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, "structcopytail_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, "structcopytail_run[cstage][%s]: exit=%d " - "want=%d (field/clobber mismatch)\n", - rows[i].label, gc, rows[i].want_exit); - fail++; - } - if (!have_ww) { - fprintf(stderr, "structcopytail_run: skip wwstage (no %s)\n", - wdrv); - continue; - } - int gw = run_build(wdrv, &rows[i], i); - if (gw != gc) { - fprintf(stderr, "structcopytail_run[%s]: cs=%d != ww=%d " - "(copy-tail divergence — #71)\n", - rows[i].label, gc, gw); - fail++; - } - if (rows[i].want_exit >= 0 && gw != rows[i].want_exit) { - fprintf(stderr, "structcopytail_run[wwstage][%s]: exit=%d " - "want=%d (field/clobber mismatch)\n", - rows[i].label, gw, rows[i].want_exit); - fail++; - } - } - - if (fail) { - fprintf(stderr, "structcopytail_run: %d/%d checks failed\n", - fail, total); - return 1; - } - printf("structcopytail_run: %d/%d ok\n", total, total); - return 0; -}