diff --git a/Makefile b/Makefile index 8b8963ab..2671dce6 100644 --- a/Makefile +++ b/Makefile @@ -594,6 +594,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_arrlit_slice_run \ $(BIN)/test_valstruct_subsize_run \ $(BIN)/test_aggret_source_run \ + $(BIN)/test_oddstruct_byval_ret_run \ $(BIN)/test_continue_run \ $(BIN)/test_callret_unsigned_arith_run \ $(BIN)/test_sar_shr_run \ @@ -3418,6 +3419,12 @@ $(BIN)/test_aggret_source_run: test/wcc/949_aggret_source_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_oddstruct_byval_ret_run: test/wcc/949_oddstruct_byval_ret_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_continue_run: test/wcc/911_continue_run.c $(BIN)/ww \ $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 32ddc3d4..318d43a7 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -12940,6 +12940,30 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) goto letlink; } } + /* #107: a struct/array call-receive whose size is a single + * eightbyte (< 8B) but NOT one of the sized-tail widths + * {1,2,4} — i.e. sz ∈ {3,5,6,7}, reachable via @packed (#51, + * e.g. @packed{u8,u32}=5) and natural sub-8 maxalign-1 shapes + * (struct{u8,u8,u8}=3) — is ONE INTEGER eightbyte returned in + * RAX (ref/qbe/amd64/sysv.c retr: the n*88) and + * cstage emitted NOTHING — the CALL was silently DROPPED, while + * wwstage emitted CALL + MOVQ AX via its generic let fallback + * (cgenstmt.ww:3007): gate-blind cs≠ww AND a silent miscompile. + * Aligns cstage UP to wwstage's SysV-correct single-MOVQ + * receive. Task #107. */ + if (n->rhs && n->rhs->kind == N_CALL && lu + && (lu->kind == TY_STRUCT || lu->kind == TY_ARRAY) + && sz < 8 + && (sz % 8 == 3 || sz % 8 == 5 + || sz % 8 == 6 || sz % 8 == 7)) { + cgexpr(c, n->rhs, *locals); + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off)); + goto letlink; + } if (n->rhs && n->rhs->kind == N_CALL && lu && (lu->kind == TY_STRUCT || lu->kind == TY_ARRAY) && sz <= 24 diff --git a/test/wcc/949_oddstruct_byval_ret_run.c b/test/wcc/949_oddstruct_byval_ret_run.c new file mode 100644 index 00000000..2ab3d0cc --- /dev/null +++ b/test/wcc/949_oddstruct_byval_ret_run.c @@ -0,0 +1,261 @@ +/* + * 949_oddstruct_byval_ret_run — task #107. A by-value RETURN of a + * struct whose total size is NOT a {1,2,4,8} sized-tail width — an + * "odd" sub-8 size 3/5/6/7 (reachable via @packed #51, and via + * natural sub-8 maxalign-1 shapes like struct{u8,u8,u8}=3) — is a + * single INTEGER eightbyte returned in RAX (ref/qbe/amd64/sysv.c + * retr: the `n*8 < size` loop runs exactly once for size ≤ 8). + * + * Pre-#107 the cstage N_LET receive (`let r: T = mk()`) fell past + * EVERY arm for these sizes: the whole-struct sized-tail arm excludes + * tails {3,5,6,7}, the memcpy arm gates on sz>8, and the no-rhs + * zero-fill requires !rhs — so cstage emitted NOTHING and SILENTLY + * DROPPED the CALL (program exit 0, reading an uninitialised slot). + * wwstage emitted `CALL; MOVQ AX, off(BP)` via its generic let + * fallback (cgenstmt.ww), so the bug was BOTH a silent miscompile AND + * a gate-blind cs≠ww divergence. The #107 fix adds a cstage arm that + * mirrors wwstage's single-MOVQ receive: the producer zero-pads AX to + * 24B and localslot rounds every slot up to 8B, so one full-word MOVQ + * captures the eightbyte cleanly. + * + * GATE POLARITY: must stay GREEN. Two independent oracles per row: + * - the runtime exit code (the load-bearing oracle: byte-id is BLIND + * to a both-wrong-identical pair, and pre-#107 cstage's drop was + * exit-0 — a green runtime here proves the CALL fires and the + * value reaches the caller, on BOTH stage drivers). + * - w6c vs w6c_ww .s byte-identity (rule 10). + * + * The expected exit is the value of the RETURNED field, COMPUTED in + * the row table (no derived size literals; the field value is the + * oracle, not a layout number) and pinned at 0..255. + * + * Size coverage targets the gap the bug lived in plus boundaries: + * 3 — struct{u8,u8,u8}, natural maxalign-1, sub-8 odd + * 5 — @packed{u8,u32}, sub-8 odd, field b crosses byte 1..4 + * 6 — struct{u16,u16,u16}, sub-8 odd, field c at byte 4 + * 7 — @packed{u8,u16,u32}, sub-8 odd, field c at byte 3..6 + * 8 — struct{i32,i32}, boundary: single full eightbyte + * 12 — struct{i32,i32,i32}, >8 two-eightbyte (sized MOVL tail) + * 24 — struct{i64,i64,i64}, three-eightbyte, top of register ABI + * The 8/12/24 rows lock that the #107 arm did NOT perturb the existing + * sized-tail / multi-word receive paths. + */ +#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[] = { + /* size 3: natural maxalign-1, return first field (byte 0). */ + { "s3_u8x3_fieldA", + "package main;\n" + "type T = struct { a: u8, b: u8, c: u8 };\n" + "fn mk() T = { return T { a = 116u8, b = 22u8, c = 33u8 }; };\n" + "export fn main() i32 = { let r: T = mk(); return r.a: i32; };\n", + 116 }, + /* size 5 (@packed{u8,u32}): return field b (bytes 1..4) — proves the + * whole eightbyte, not just AX's low byte, reaches the caller. */ + { "s5_packed_fieldB", + "package main;\n" + "type T = struct @packed { a: u8, b: u32 };\n" + "fn mk() T = { return T { a = 9u8, b = 116u32 }; };\n" + "export fn main() i32 = { let r: T = mk(); return r.b: i32; };\n", + 116 }, + /* size 6 (3x u16): return field c (bytes 4..5). */ + { "s6_u16x3_fieldC", + "package main;\n" + "type T = struct { a: u16, b: u16, c: u16 };\n" + "fn mk() T = { return T { a = 9u16, b = 22u16, c = 116u16 }; };\n" + "export fn main() i32 = { let r: T = mk(); return r.c: i32; };\n", + 116 }, + /* size 7 (@packed{u8,u16,u32}): return field c (bytes 3..6). */ + { "s7_packed_fieldC", + "package main;\n" + "type T = struct @packed { a: u8, b: u16, c: u32 };\n" + "fn mk() T = { return T { a = 9u8, b = 22u16, c = 116u32 }; };\n" + "export fn main() i32 = { let r: T = mk(); return r.c: i32; };\n", + 116 }, + /* size 8 boundary: single full eightbyte, return first field. */ + { "s8_i32x2_fieldA", + "package main;\n" + "type T = struct { a: i32, b: i32 };\n" + "fn mk() T = { return T { a = 116, b = 22 }; };\n" + "export fn main() i32 = { let r: T = mk(); return r.a; };\n", + 116 }, + /* size 12: two eightbytes (MOVQ + MOVL tail) — unchanged by #107. */ + { "s12_i32x3_fieldC", + "package main;\n" + "type T = struct { a: i32, b: i32, c: i32 };\n" + "fn mk() T = { return T { a = 9, b = 22, c = 116 }; };\n" + "export fn main() i32 = { let r: T = mk(); return r.c; };\n", + 116 }, + /* size 24: three eightbytes, top of the register-return ABI. */ + { "s24_i64x3_fieldC", + "package main;\n" + "type T = struct { a: i64, b: i64, c: i64 };\n" + "fn mk() T = { return T { a = 9i64, b = 22i64, c = 116i64 }; };\n" + "export fn main() i32 = { let r: T = mk(); return r.c: i32; };\n", + 116 }, +}; + +static int +run_driver(const char *driver, const struct row *r, int i) +{ + char src[64], tmpdir[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/oddret_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/oddret_%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 && timeout 180 %s build %s 2>/dev/null", + tmpdir, driver, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: build via %s failed\n", + r->label, driver); + unlink(src); rmdir(tmpdir); + return -2; + } + + 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 = runwait(outbin); + + unlink(src); unlink(outbin); rmdir(tmpdir); + return got; +} + +/* w6c (cstage) vs w6c_ww (wwstage) .s byte-id (rule 10). */ +static int +asm_byte_identical(const char *bin, const struct row *r, int i) +{ + char src[64], cs[64], ws[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/oddret_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/oddret_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/oddret_asm_%d_%d_w.s", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + snprintf(cmd, sizeof cmd, "timeout 180 %s/w6c -o %s %s 2>/dev/null", + bin, cs, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c errored\n", r->label); + unlink(src); + return -1; + } + snprintf(cmd, sizeof cmd, "timeout 180 %s/w6c_ww -o %s %s 2>/dev/null", + bin, ws, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c_ww errored\n", r->label); + unlink(src); unlink(cs); + return -1; + } + + FILE *fc = fopen(cs, "rb"); + FILE *fw = fopen(ws, "rb"); + int rc = 0; + if (!fc || !fw) { + rc = -1; + } else { + for (;;) { + int a = fgetc(fc); + int b = fgetc(fw); + if (a != b) { rc = -1; break; } + if (a == EOF) break; + } + } + if (fc) fclose(fc); + if (fw) fclose(fw); + if (rc != 0) + fprintf(stderr, "row[%s]: cstage vs wwstage asm differs\n", + r->label); + unlink(src); unlink(cs); unlink(ws); + return rc; +} + +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); + + struct { const char *name; const char *path; int gated; } + 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 && access(drivers[d].path, X_OK) != 0) { + fprintf(stderr, "oddstruct_byval_ret: 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, + "oddstruct_byval_ret[%s][%s]: exit=%d want=%d\n", + drivers[d].name, rows[i].label, + got, rows[i].want); + fail++; + } + } + } + + if (access(wdrv, X_OK) == 0) { + for (int i = 0; i < n; i++) { + total++; + if (asm_byte_identical(bin, &rows[i], i) != 0) + fail++; + } + } + + if (fail) { + fprintf(stderr, + "oddstruct_byval_ret: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("oddstruct_byval_ret: %d/%d ok\n", total, total); + return 0; +}