test: migrate structcopytail #73 struct-copy-tail family to test/lang @test, retire C twin (fold-2)

Continue fold-2: migrate the #73 whole-struct field-copy ragged-tail test from a
bespoke build+run C twin to test/lang @test, retiring the twin in the same
commit. Runtime coverage MOVES from $(TESTS) to test-lang (T1 runs+asserts via
`ww test`) + test-lang-byteid (T2 keeps cs==ww .s byte-id); coverage is
preserved, the $(TESTS) headline drops 1 (422->421). All four rows are pure
value-rows (no reject rows): each poisons `mark` at the inner struct's natural
offset, copies, then asserts every field back with primitive int comparisons, so
an 8-byte MOVQ that over-writes the ragged-tail successor FAILS.

  989_structcopytail_run.c -> structcopytail_test.ww (tail2/tail6/tail7 ragged-tail copy preserves mark; ctl8 8-aligned control)

This is the only Family-3 file with no build-must-fail rows; the three
compound-OP= twins (948_idx_compound, 949_dotfield_compound, 949_idxfield_compound)
carry loud //ww:error reject rows a runtime @test cannot replicate and are
DEFERRED to a fold-3 value-split + reject-carrier pass (they stay fully in
$(TESTS), no coverage lost).

Bump LANGBYTEID_EXPECTED_MIN 34->35 to ratchet the new corpus floor.
This commit is contained in:
2026-06-22 12:11:46 +09:00
parent a367bf984e
commit b879d38b0f
3 changed files with 77 additions and 253 deletions

View File

@@ -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; \

View File

@@ -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);
};

View File

@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
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;
}