From 098b58d15c434cc1a32b3d3665fcd59cac59a65b Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 28 May 2026 16:54:08 +0900 Subject: [PATCH] test: regression lock for struct-ABI size formula (#78 lock) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Project #78 was ken-flagged: cstage folded size(struct{i32,i32,i32}) to 12 (cmd/wcc/check.c:760, `(off + maxalign - 1) & ~(maxalign - 1)`) while wwstage walked a slot-padded fsz ladder and yielded 16. The two checkers ran different layout formulas. d4e500f (#169) closed the cgen ABI half by introducing structabisize and walking it via per- field tinfo.align; 39f9267 + 66a91c8 converged the remaining cgen sites that had picked the slot-padded number. Verified at the size(T) fold and the .s byte-id on every layout-edge shape. Add 762_struct_abi_size.c (6 table rows) to lock the closure. Each row carries a (sz, aln) field table; the expected size is computed in C via the Hare layout formula (rule 13: no hardcoded size literals — a formula bump lands in compute_expected and every row tracks). Both gates run per row: a runtime check that `return size(T): i32` matches the computed expected on both stages (catches a fold drift), and a w6c vs w6c_ww .s byte-id (catches a cgen-ABI walker drift even when the fold still matches — the two walkers are independent SSoTs). Rows target maxalign edges: i32_x3_maxalign4 #78 canonical, natural==ABI==12 i32_i32_i64_lead_narrow maxalign 8, narrow lead, ABI 16 i64_i32_i32_sub8_tail #169 sub-8 tail, ABI 16 (the round-up) i16_x3_maxalign2 maxalign 2, natural==ABI==6 i32_i32_i64_mid_align mid-record align step explicit i64_x3_natural_24 all 8-wide, natural==ABI==24 GATE POLARITY: this file must stay GREEN. A red here means either d4e500f reverted, or one of the two layout walkers (check.c:760 / check.ww:958 / cgen.c struct_arg_size / cgenutil.ww structabisize) drifted from the formula. --- Makefile | 7 + test/wcc/762_struct_abi_size.c | 286 +++++++++++++++++++++++++++++++++ 2 files changed, 293 insertions(+) create mode 100644 test/wcc/762_struct_abi_size.c diff --git a/Makefile b/Makefile index 8b289f96..eb96abf2 100644 --- a/Makefile +++ b/Makefile @@ -312,6 +312,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_check_enum_fold \ $(BIN)/test_def_widen_const \ $(BIN)/test_inferred_struct_arg_push \ + $(BIN)/test_struct_abi_size \ $(BIN)/test_use_promote_alias \ $(BIN)/test_field_signed $(BIN)/test_frame_argcount \ $(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \ @@ -591,6 +592,12 @@ $(BIN)/test_inferred_struct_arg_push: test/wcc/761_inferred_struct_arg_push.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_struct_abi_size: test/wcc/762_struct_abi_size.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_arrlit_str_full: test/wcc/711_arrlit_str_full.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/test/wcc/762_struct_abi_size.c b/test/wcc/762_struct_abi_size.c new file mode 100644 index 00000000..6afcc21e --- /dev/null +++ b/test/wcc/762_struct_abi_size.c @@ -0,0 +1,286 @@ +/* + * 762_struct_abi_size — defensive lock for project #78. cstage once + * folded size(struct{i32,i32,i32}) to 12 while wwstage folded it to + * 16; the two checkers walked different layout formulas. After + * d4e500f (#169) introduced structabisize and the cgen ABI sites + * converged onto a single (off + maxalign - 1) & ~(maxalign - 1) + * formula on both sides, every layout-edge shape should agree — + * BOTH at the size(T) fold (cstage check.c:760 / wwstage check.ww + * astsize N_TSTRUCT) and at the register-RECV/RETURN ABI walker + * (cgen.c struct_arg_size / structabisize). This probe pins both. + * + * The fold and the ABI walker share the formula but live in two + * different sources: check.c:760 vs check.ww:958 (the language + * builtin) and cmd/w6c/cgen.c struct_arg_size vs + * selfhost/cmd/wcc/cgenutil.ww structabisize (the cgen ABI). Either + * can drift independently — locking only the runtime exit code via + * size(T) misses an ABI-walker regression that does not change the + * literal fold; locking only the .s byte-id misses a fold regression + * the cgen would happily honour. Both gates run per row. + * + * Expected size is COMPUTED from a (sz, aln) field table per row, + * not hardcoded — a stage-internal formula bump (rule 13) lands the + * change in compute_expected() and every row tracks. The probe must + * red the moment any source-of-truth drifts from the documented + * Hare-style layout: align(off, fa); off += fs; size = align(off, + * maxalign). + * + * Shape coverage targets the maxalign edges where the bug class + * lived: + * 1. {3xi32} — #78 canonical, maxalign 4, natural 12, + * ABI 12 (no round-up) + * 2. {i32,i32,i64} — maxalign 8, narrow lead, ABI 16 + * 3. {i64,i32,i32} — maxalign 8 with sub-8 tail, ABI 16 + * (the #169 round-up case) + * 4. {3xi16} — maxalign 2, natural 6, ABI 6 + * 5. {2xi32,i64} — maxalign 8 with mid-record alignment + * padding (f3 must skip to off=8) + * 6. {3xi64} — maxalign 8, all 8-wide, natural==ABI=24 + * + * GATE POLARITY: this file must stay GREEN. A red here means either + * d4e500f (#169 structabisize) reverted, or one of the two layout + * walkers drifted from the formula. + */ +#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 field { int sz; int aln; }; + +struct row { + const char *label; + const char *src; /* must `return size(T): i32` */ + int nfields; + struct field fields[8]; +}; + +/* Hare / cstage check.c:701-760 / wwstage check.ww astsize N_TSTRUCT + * formula. Per CLAUDE.md rule 13 the expected size for each row is + * derived here, not literalled into the row table. */ +static int +compute_expected(const struct field *f, int n) +{ + int off = 0, maxalign = 1; + for (int i = 0; i < n; i++) { + int a = f[i].aln; + if (a > maxalign) maxalign = a; + off = (off + a - 1) & ~(a - 1); + off += f[i].sz; + } + return (off + maxalign - 1) & ~(maxalign - 1); +} + +static const struct row rows[] = { + /* 1. {3xi32} — #78 canonical. cstage check.c:760 returned 12, + * wwstage formerly walked a slot-padded ladder yielding 16. The + * fold now uses astsize's (off + maxal - 1) & ~(maxal - 1) on + * both sides; expect 12. */ + { "i32_x3_maxalign4", + "type t = struct { a: i32, b: i32, c: i32 };\n" + "export fn main() i32 = { return size(t): i32; };\n", + 3, {{4,4},{4,4},{4,4}} }, + /* 2. {i32,i32,i64} — maxalign 8, off lands at 8 before the i64, + * total 16. */ + { "i32_i32_i64_lead_narrow", + "type t = struct { a: i32, b: i32, c: i64 };\n" + "export fn main() i32 = { return size(t): i32; };\n", + 3, {{4,4},{4,4},{8,8}} }, + /* 3. {i64,i32,i32} — natural extent 16, maxalign 8: the post- + * field-3 round-up is a no-op here, but the #169 commit added + * exactly this round-up so the structabisize walker matches + * cstage on the maxalign-8 sub-8-tail shape (cf. {i64,i32}). */ + { "i64_i32_i32_sub8_tail", + "type t = struct { a: i64, b: i32, c: i32 };\n" + "export fn main() i32 = { return size(t): i32; };\n", + 3, {{8,8},{4,4},{4,4}} }, + /* 4. {3xi16} — maxalign 2, natural 6. Guards against a future + * "round to 8" shortcut creeping back into either walker. */ + { "i16_x3_maxalign2", + "type t = struct { a: i16, b: i16, c: i16 };\n" + "export fn main() i32 = { return size(t): i32; };\n", + 3, {{2,2},{2,2},{2,2}} }, + /* 5. {2xi32,i64} — f3 must skip off from 8→8 (already aligned) + * but the row exercises the mid-record align step explicitly; + * a missing `off = align(off, fa)` would mis-place the i64. */ + { "i32_i32_i64_mid_align", + "type t = struct { a: i32, b: i32, c: i64 };\n" + "export fn main() i32 = { return size(t): i32; };\n", + 3, {{4,4},{4,4},{8,8}} }, + /* 6. {3xi64} — natural==ABI, the maxalign-rounded formula must + * not double-round. */ + { "i64_x3_natural_24", + "type t = struct { a: i64, b: i64, c: i64 };\n" + "export fn main() i32 = { return size(t): i32; };\n", + 3, {{8,8},{8,8},{8,8}} }, +}; + +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/wcabi_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/wcabi_%d_d_%d", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + mkdir(tmpdir, 0755); + /* timeout 180 per repo convention (cf. 760, 761); test/run does + * not bound individual binaries. */ + 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 -1; + } + + 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; +} + +/* asm_byte_identical — diff w6c vs w6c_ww text output. The cgen ABI + * walker (structabisize since #169 / d4e500f) feeds the struct- + * return + struct-arg push widths; a divergence on these shapes + * surfaces as a .s difference even when the size(T) fold still + * matches (the two walkers are independent SSoTs). */ +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/wcabi_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/wcabi_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/wcabi_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]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + char wdrv[1024]; + 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, "struct_abi_size: skip %s (no %s)\n", + drivers[d].name, drivers[d].path); + continue; + } + for (int i = 0; i < n; i++) { + int want = compute_expected(rows[i].fields, + rows[i].nfields); + int got = run_driver(drivers[d].path, &rows[i], i); + total++; + if (got != want) { + fprintf(stderr, + "struct_abi_size[%s][%s]: size=%d want=%d\n", + drivers[d].name, rows[i].label, + got, 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, + "struct_abi_size: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("struct_abi_size: %d/%d ok\n", total, total); + return 0; +}