From 92cd573197b8e97b138c1f401a5f7790b3314834 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sat, 13 Jun 2026 04:31:12 +0900 Subject: [PATCH] wcc: defer capacity 32 with a loud cap error, both stages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wwstage capped defers at 16 and SILENTLY DROPPED the 17th; cstage capped at 32. Align the cap at 32 and make exceeding it a loud compile error in BOTH stages — the silent 16-vs-32 split was the bug (a defer that never runs is a leaked resource). Both stages move in one commit: one cap contract. --- Makefile | 12 ++ cmd/w6c/cgen.c | 8 +- selfhost/cmd/w6c/main.combined.ww | 15 ++- selfhost/cmd/wcc/cgen.ww | 2 +- selfhost/cmd/wcc/cgenstmt.ww | 13 +- selfhost/cmd/wwdump/main.combined.ww | 15 ++- test/wcc/989_defercap_run.c | 180 +++++++++++++++++++++++++++ 7 files changed, 230 insertions(+), 15 deletions(-) create mode 100644 test/wcc/989_defercap_run.c diff --git a/Makefile b/Makefile index be18fc30..4260809f 100644 --- a/Makefile +++ b/Makefile @@ -260,6 +260,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_defdim_field_run \ $(BIN)/test_defdim_argslice_run \ $(BIN)/test_slttypepref_run \ + $(BIN)/test_defercap_run \ $(BIN)/test_ampfncollide_run \ $(BIN)/test_trycallcollide_run \ $(BIN)/test_gunsigned_run \ @@ -794,6 +795,17 @@ $(BIN)/test_slttypepref_run: test/wcc/989_slttypepref_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# 989_defercap_run (#40, F13 c1): the defer stack fails loud at its shared +# cap (32) in BOTH stages instead of silently dropping the overflowing +# deferred call (wwstage was capped at 16). Builds/rejects on both driver +# twins (rule-10). See the test header. +$(BIN)/test_defercap_run: test/wcc/989_defercap_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_ampfncollide_run (#4, c2): `&fn` synthesis (unoptype TK_AMP + # assignableaddrfn) prefers the current module's fn when a same-leaf fn is # declared in a later module. Builds/rejects on BOTH driver twins (rule-10). diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 457ac0a6..f126b079 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -14725,9 +14725,11 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) break; } case N_DEFER: - if (ndefers < DEFER_MAX) { - defers[ndefers++] = n->lhs; - } + /* #40: at the cap, fail loud rather than silently drop the + * deferred call (the ww twin in cgenstmt.ww fatals too). */ + if (ndefers >= DEFER_MAX) + fatal("cgen: too many defers in one function"); + defers[ndefers++] = n->lhs; break; case N_YIELD: /* Evaluate the value into AX, then jump to the enclosing diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index afaa394b..970e9f59 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -35682,10 +35682,17 @@ fn cgstmt(c: *cgen, n: *node) void = { if (k == nkind.N_YIELD) { cgyield(c, n); return; }; if (k == nkind.N_DEFER) { - if (c.defertop < DEFER_MAX) { - c.deferbuf[c.defertop] = n.lhs; - c.defertop += 1; + // #40: at the cap, fail loud in BOTH stages rather than + // silently drop the deferred call. cstage's DEFER_MAX was 32 + // and also dropped silently past it; the runtime-correct target + // is a hard stop at the shared cap (cgen.c twin fatals too). + if (c.defertop >= DEFER_MAX) { + let msg: str = "cgen: too many defers in one function\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); }; + c.deferbuf[c.defertop] = n.lhs; + c.defertop += 1; return; }; @@ -41060,7 +41067,7 @@ type enumtype = struct { }; def LOOP_MAX: i32 = 16; -def DEFER_MAX: i32 = 16; +def DEFER_MAX: i32 = 32; // #40: match cstage cgen.c DEFER_MAX (shared cap) // The SysV register-return-ABI caps — the SINGLE SSoT shared by the sret // classifier (sretretsize over-cap-tuple arm) AND every emit/receive site diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index e633c3f7..854ececd 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -448,7 +448,7 @@ type enumtype = struct { }; def LOOP_MAX: i32 = 16; -def DEFER_MAX: i32 = 16; +def DEFER_MAX: i32 = 32; // #40: match cstage cgen.c DEFER_MAX (shared cap) // The SysV register-return-ABI caps — the SINGLE SSoT shared by the sret // classifier (sretretsize over-cap-tuple arm) AND every emit/receive site diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index cbc01023..b14acc54 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -49,10 +49,17 @@ fn cgstmt(c: *cgen, n: *node) void = { if (k == nkind.N_YIELD) { cgyield(c, n); return; }; if (k == nkind.N_DEFER) { - if (c.defertop < DEFER_MAX) { - c.deferbuf[c.defertop] = n.lhs; - c.defertop += 1; + // #40: at the cap, fail loud in BOTH stages rather than + // silently drop the deferred call. cstage's DEFER_MAX was 32 + // and also dropped silently past it; the runtime-correct target + // is a hard stop at the shared cap (cgen.c twin fatals too). + if (c.defertop >= DEFER_MAX) { + let msg: str = "cgen: too many defers in one function\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); }; + c.deferbuf[c.defertop] = n.lhs; + c.defertop += 1; return; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 204ffe1f..c01d0271 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -35682,10 +35682,17 @@ fn cgstmt(c: *cgen, n: *node) void = { if (k == nkind.N_YIELD) { cgyield(c, n); return; }; if (k == nkind.N_DEFER) { - if (c.defertop < DEFER_MAX) { - c.deferbuf[c.defertop] = n.lhs; - c.defertop += 1; + // #40: at the cap, fail loud in BOTH stages rather than + // silently drop the deferred call. cstage's DEFER_MAX was 32 + // and also dropped silently past it; the runtime-correct target + // is a hard stop at the shared cap (cgen.c twin fatals too). + if (c.defertop >= DEFER_MAX) { + let msg: str = "cgen: too many defers in one function\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); }; + c.deferbuf[c.defertop] = n.lhs; + c.defertop += 1; return; }; @@ -41060,7 +41067,7 @@ type enumtype = struct { }; def LOOP_MAX: i32 = 16; -def DEFER_MAX: i32 = 16; +def DEFER_MAX: i32 = 32; // #40: match cstage cgen.c DEFER_MAX (shared cap) // The SysV register-return-ABI caps — the SINGLE SSoT shared by the sret // classifier (sretretsize over-cap-tuple arm) AND every emit/receive site diff --git a/test/wcc/989_defercap_run.c b/test/wcc/989_defercap_run.c new file mode 100644 index 00000000..a22576ed --- /dev/null +++ b/test/wcc/989_defercap_run.c @@ -0,0 +1,180 @@ +/* + * 989_defercap_run (#40, F13 c1) — the defer stack must fail loud at its cap + * in BOTH stages, not silently drop the overflowing deferred call. + * + * THE BUG (cat-A, both stages diverged AND both wrong at their cap): wwstage + * cgstmt N_DEFER pushed only while defertop < DEFER_MAX where DEFER_MAX was 16 + * (cgen.ww), so a function with 17 defers silently dropped the 17th — the + * counter reached 16, exit 1, while cstage (DEFER_MAX 32) emitted all 17. + * cstage in turn silently dropped its own 33rd defer. The runtime-correct + * target (CAP-SEMANTICS rule: cstage silent at its own cap => loud both): + * the cap is the shared 32, and a function exceeding it is a hard compile + * error in BOTH stages (cgenstmt.ww N_DEFER + cgen.c N_DEFER fatal). + * + * row | ndefers | shape | result (cs==ww) + * ------------+---------+-------------------------------+---------------- + * seventeen | 17 | original repro (>old ww 16) | exit 0 + * at_cap_32 | 32 | exactly the shared cap | exit 0 + * over_cap_33 | 33 | one past the cap | build FAILS loud + * + * seventeen was RED pre-c1 on wwstage (counter 16, exit 1 vs cstage exit 0). + * over_cap_33 was RED pre-c1 on BOTH stages (silent rc=0 accept dropping the + * overflowing defer). at_cap_32 pins the boundary stays accepted byte-id. + */ +#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; int ndefers; int want_build_fail; int want_exit; }; + +static const struct row rows[] = { + { "seventeen", 17, 0, 0 }, + { "at_cap_32", 32, 0, 0 }, + { "over_cap_33", 33, 1, 0 }, +}; + +/* Emit `package main` with a doit() carrying ndefers `defer bump();` and a + * main() that checks the global counter equals ndefers. */ +static int +write_src(const char *path, int ndefers) +{ + FILE *f = fopen(path, "wb"); + if (!f) return -1; + fputs("package main;\n" + "let cnt: i32 = 0;\n" + "fn bump() void = { cnt = cnt + 1; };\n" + "fn doit() void = {\n", f); + for (int i = 0; i < ndefers; i++) + fputs("\tdefer bump();\n", f); + fprintf(f, + "};\n" + "export fn main() i32 = {\n" + "\tdoit();\n" + "\tif (cnt == %d) { return 0; };\n" + "\treturn 1;\n" + "};\n", ndefers); + fclose(f); + return 0; +} + +/* Returns the build rc in *brc; the run exit code (or -1) as the value. */ +static int +build_run(const char *driver, const struct row *r, int i, int *brc) +{ + char src[64], tmpdir[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/dcap_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/dcap_%d_d_%d", getpid(), i); + + if (write_src(src, r->ndefers) != 0) { *brc = -1; return -1; } + + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>/dev/null", + tmpdir, driver, src); + *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 got; +} + +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 cbrc, gc = build_run(cdrv, &rows[i], i, &cbrc); + + if (rows[i].want_build_fail) { + if (cbrc == 0) { + fprintf(stderr, "defercap[cstage][%s]: built rc=0, " + "expected loud build failure (#40)\n", rows[i].label); + fail++; + } + } else { + if (cbrc != 0 || gc != rows[i].want_exit) { + fprintf(stderr, "defercap[cstage][%s]: brc=%d exit=%d " + "want_exit=%d\n", rows[i].label, cbrc, gc, + rows[i].want_exit); + fail++; + } + } + + if (!have_ww) { + fprintf(stderr, "defercap: skip wwstage (no %s)\n", wdrv); + continue; + } + int wbrc, gw = build_run(wdrv, &rows[i], i, &wbrc); + + if (rows[i].want_build_fail) { + if (wbrc == 0) { + fprintf(stderr, "defercap[wwstage][%s]: built rc=0, " + "expected loud build failure (#40)\n", rows[i].label); + fail++; + } + /* both-fail is the rule-10 agreement here */ + if ((cbrc == 0) != (wbrc == 0)) { + fprintf(stderr, "defercap[%s]: build-fail divergence " + "cs_rc=%d ww_rc=%d (#40)\n", rows[i].label, cbrc, wbrc); + fail++; + } + } else { + if (wbrc != 0 || gw != rows[i].want_exit) { + fprintf(stderr, "defercap[wwstage][%s]: brc=%d exit=%d " + "want_exit=%d\n", rows[i].label, wbrc, gw, + rows[i].want_exit); + fail++; + } + if (gw != gc) { + fprintf(stderr, "defercap[%s]: cs=%d != ww=%d " + "(defer-cap divergence — #40)\n", rows[i].label, gc, gw); + fail++; + } + } + } + + if (fail) { + fprintf(stderr, "defercap_run: %d/%d checks failed\n", fail, total); + return 1; + } + printf("defercap_run: %d/%d ok\n", total, total); + return 0; +}