From 5ed6293330ce13b8df123d7f46d3a9924378b991 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 19 May 2026 04:04:41 +0900 Subject: [PATCH] selfhost+test: bump wwstage varargseq per cgcall (#8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Latent surface from #15: cgcall variadic-gather block read seq from n.uval, which post-#15 is always 0 because scanlocals (which used to stamp it during pre-pass) was deleted. Every variadic callsite in a fn aliased to @vararg_d_0 / @vararg_sl_0. When two callsites in one fn had differing arities, the second hit #15's first-use+fail-loud guard ("localadd: @-prefix slot grew within fn") — correctly, since the slot was being asked to grow mid-fn. Fix: read seq from c.varargseq + bump in cgcall's gather branch. Mirrors cstage's mklabel("vararg_d/sl") natural seq bump. cgeninit zeroes c.varargseq per-fn (existing), so the counter is correctly per-fn scoped. cgen.ww varargseq comment refreshed — replaces stale "bumped only at emit time" misclaim with the post-#15 per-call shape + the #15 grow-on-pin discipline that surfaced the wedge. 751_vararg_seq_percall: table-driven 3 rows x 2 stages = 6 fixtures. mixed_arity_two_calls (the wedge), same_arity_two_calls (no-regress), three_arity_drift (1/2/3 mints @vararg_d_0/1/2). make test 125/125; ww2==ww3==ww4 byte-id holds via 995_self_rebuild. Surfaced by worker-strcontains2 attempting strings.contains tagged- variadic graduation — mixed-arity spec test rows triggered the wedge. Unblocks #9 + #10 (strings/bytes.contains). --- Makefile | 7 + selfhost/cmd/w6c/main.combined.ww | 18 ++- selfhost/cmd/wcc/cgen.ww | 10 +- selfhost/cmd/wcc/cgenexpr.ww | 8 +- selfhost/cmd/wwdump/main.combined.ww | 18 ++- test/wcc/751_vararg_seq_percall.c | 198 +++++++++++++++++++++++++++ 6 files changed, 238 insertions(+), 21 deletions(-) create mode 100644 test/wcc/751_vararg_seq_percall.c diff --git a/Makefile b/Makefile index 74ec50a2..609cc477 100644 --- a/Makefile +++ b/Makefile @@ -285,6 +285,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_size_strategy_convergence \ $(BIN)/test_sumtype_forward \ $(BIN)/test_mklabel_modscoped \ + $(BIN)/test_vararg_seq_percall \ $(BIN)/test_param_shadow_mod \ $(BIN)/test_localoff_scope \ $(BIN)/test_cast_enum_movl \ @@ -720,6 +721,12 @@ $(BIN)/test_mklabel_modscoped: test/wcc/750_mklabel_modscoped.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_vararg_seq_percall: test/wcc/751_vararg_seq_percall.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_match_4arm_cross_module_run: test/wcc/929_match_4arm_cross_module_run.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index e2d063e8..3c9c03b0 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -13906,8 +13906,9 @@ fn cgcall(c: *cgen, n: *node) void = { // of the call machinery sees one slice slot for the variadic. // Forwarding shape (`xs...`) skips the gather: the spread's // inner slice expression replaces the wrapper in place. Empty - // (no trailing args) writes a {nil, 0, 0} descriptor. The seq - // matches the one cgcall stamped on n.uval at first emit. + // (no trailing args) writes a {nil, 0, 0} descriptor. Per-call + // seq comes from c.varargseq bumped at gather emit (mirrors + // cstage's mklabel("vararg_d/sl") freshness). { let nfixed_v: i32 = 0; let varp: *node = callee_variadic_param(c, callee, &nfixed_v); @@ -13945,7 +13946,8 @@ fn cgcall(c: *cgen, n: *node) void = { if (prev == nil) { n.list = inner; } else { prev.next = inner; }; } else { - let seq: i32 = n.uval: i32; + let seq: i32 = c.varargseq; + c.varargseq += 1; let dname: str = mkvarargname(c, "@vararg_d_", seq); let sname: str = mkvarargname(c, "@vararg_sl_", seq); // Use raw element size, not stack-padded @@ -19083,10 +19085,12 @@ type cgen = struct { yieldbuf: *str, // stack of match end labels for yield defertop: i32, deferbuf: **node, // stack of deferred exprs (LIFO at return) - // Variadic-call gather state. cgcall assigns per-call scratch - // names `@vararg_d_N` / `@vararg_sl_N` using this counter; - // post #15 the seq is bumped only at emit time so a single - // sequence is observed (the scanlocals pre-pass was dropped). + // Variadic-call gather state. cgcall bumps this on each gather + // emit and uses it to mint `@vararg_d_N` / `@vararg_sl_N` per + // callsite; mirrors cstage's mklabel("vararg_d/sl") freshness + // so two variadic callsites with different arities in one fn + // get distinct slots (the shared slot fail-louds under #15's + // @-prefix grow-on-pin discipline). varargseq: i32, // System V AMD64 sret discipline (#23). Plain TY_STRUCT returns // with size > 24B are passed via a hidden first-arg pointer diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index 8d3ccd3b..935437a9 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -469,10 +469,12 @@ type cgen = struct { yieldbuf: *str, // stack of match end labels for yield defertop: i32, deferbuf: **node, // stack of deferred exprs (LIFO at return) - // Variadic-call gather state. cgcall assigns per-call scratch - // names `@vararg_d_N` / `@vararg_sl_N` using this counter; - // post #15 the seq is bumped only at emit time so a single - // sequence is observed (the scanlocals pre-pass was dropped). + // Variadic-call gather state. cgcall bumps this on each gather + // emit and uses it to mint `@vararg_d_N` / `@vararg_sl_N` per + // callsite; mirrors cstage's mklabel("vararg_d/sl") freshness + // so two variadic callsites with different arities in one fn + // get distinct slots (the shared slot fail-louds under #15's + // @-prefix grow-on-pin discipline). varargseq: i32, // System V AMD64 sret discipline (#23). Plain TY_STRUCT returns // with size > 24B are passed via a hidden first-arg pointer diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 48761aeb..faae00a2 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -2940,8 +2940,9 @@ fn cgcall(c: *cgen, n: *node) void = { // of the call machinery sees one slice slot for the variadic. // Forwarding shape (`xs...`) skips the gather: the spread's // inner slice expression replaces the wrapper in place. Empty - // (no trailing args) writes a {nil, 0, 0} descriptor. The seq - // matches the one cgcall stamped on n.uval at first emit. + // (no trailing args) writes a {nil, 0, 0} descriptor. Per-call + // seq comes from c.varargseq bumped at gather emit (mirrors + // cstage's mklabel("vararg_d/sl") freshness). { let nfixed_v: i32 = 0; let varp: *node = callee_variadic_param(c, callee, &nfixed_v); @@ -2979,7 +2980,8 @@ fn cgcall(c: *cgen, n: *node) void = { if (prev == nil) { n.list = inner; } else { prev.next = inner; }; } else { - let seq: i32 = n.uval: i32; + let seq: i32 = c.varargseq; + c.varargseq += 1; let dname: str = mkvarargname(c, "@vararg_d_", seq); let sname: str = mkvarargname(c, "@vararg_sl_", seq); // Use raw element size, not stack-padded diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index f656e404..f1b38fe3 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -13906,8 +13906,9 @@ fn cgcall(c: *cgen, n: *node) void = { // of the call machinery sees one slice slot for the variadic. // Forwarding shape (`xs...`) skips the gather: the spread's // inner slice expression replaces the wrapper in place. Empty - // (no trailing args) writes a {nil, 0, 0} descriptor. The seq - // matches the one cgcall stamped on n.uval at first emit. + // (no trailing args) writes a {nil, 0, 0} descriptor. Per-call + // seq comes from c.varargseq bumped at gather emit (mirrors + // cstage's mklabel("vararg_d/sl") freshness). { let nfixed_v: i32 = 0; let varp: *node = callee_variadic_param(c, callee, &nfixed_v); @@ -13945,7 +13946,8 @@ fn cgcall(c: *cgen, n: *node) void = { if (prev == nil) { n.list = inner; } else { prev.next = inner; }; } else { - let seq: i32 = n.uval: i32; + let seq: i32 = c.varargseq; + c.varargseq += 1; let dname: str = mkvarargname(c, "@vararg_d_", seq); let sname: str = mkvarargname(c, "@vararg_sl_", seq); // Use raw element size, not stack-padded @@ -19083,10 +19085,12 @@ type cgen = struct { yieldbuf: *str, // stack of match end labels for yield defertop: i32, deferbuf: **node, // stack of deferred exprs (LIFO at return) - // Variadic-call gather state. cgcall assigns per-call scratch - // names `@vararg_d_N` / `@vararg_sl_N` using this counter; - // post #15 the seq is bumped only at emit time so a single - // sequence is observed (the scanlocals pre-pass was dropped). + // Variadic-call gather state. cgcall bumps this on each gather + // emit and uses it to mint `@vararg_d_N` / `@vararg_sl_N` per + // callsite; mirrors cstage's mklabel("vararg_d/sl") freshness + // so two variadic callsites with different arities in one fn + // get distinct slots (the shared slot fail-louds under #15's + // @-prefix grow-on-pin discipline). varargseq: i32, // System V AMD64 sret discipline (#23). Plain TY_STRUCT returns // with size > 24B are passed via a hidden first-arg pointer diff --git a/test/wcc/751_vararg_seq_percall.c b/test/wcc/751_vararg_seq_percall.c new file mode 100644 index 00000000..17695a7f --- /dev/null +++ b/test/wcc/751_vararg_seq_percall.c @@ -0,0 +1,198 @@ +/* + * 751_vararg_seq_percall — sentinel for task #8. Post-#15 wwstage's + * cgcall never bumped c.varargseq, so every variadic callsite in a fn + * read seq=0 and minted the same `@vararg_d_0` / `@vararg_sl_0` slot. + * Two variadic callsites with different arities in one fn hit #15's + * `localadd: @-prefix slot grew within fn` fail-loud — the second + * gather asked for a different element-buffer size than the pinned + * slot. Cstage was unaffected: its `mklabel("vararg_d/sl")` bumps + * labelseq naturally per call. + * + * Fix: cgcall reads `c.varargseq` and bumps it at each gather emit; + * mirrors cstage's mklabel freshness. + * + * row | what it pins + * -----------------------------+--------------------------------- + * mixed_arity_two_calls | original wedge — pick(1 arg) + + * | pick(3 args) in main. Pre-fix + * | wwstage fatals at the second + * | gather; cstage rc=0. Post-fix + * | both rc=0. + * same_arity_two_calls | non-regression — pre and post + * | both stages rc=0; same arity + * | reuses the same slot size so + * | the size-grow check never fired. + * three_arity_drift | 3 callsites with arities 1/2/3 + * | in one fn. Post-fix both stages + * | rc=0 and three distinct seq slots + * | get minted (`@vararg_d_0/1/2`). + */ +#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[] = { + /* 1. Two-call arity drift. Pre-fix wwstage fatals on the second + * gather (1 rune vs 3 runes -> different element-buffer size). */ + { "mixed_arity_two_calls", + "package main;\n" + "import os;\n" + "fn pick(args: (str | rune)...) i32 = { return args.len; };\n" + "export fn main() i32 = {\n" + " if (pick(\"a\") != 1) { os.exit(11); };\n" + " if (pick(\"a\", \"b\", \"c\") != 3) { os.exit(12); };\n" + " return 0;\n" + "};\n", + 0 }, + + /* 2. Non-regression: same-arity calls. */ + { "same_arity_two_calls", + "package main;\n" + "import os;\n" + "fn pick(args: (str | rune)...) i32 = { return args.len; };\n" + "export fn main() i32 = {\n" + " if (pick(\"a\") != 1) { os.exit(11); };\n" + " if (pick(\"b\") != 1) { os.exit(12); };\n" + " return 0;\n" + "};\n", + 0 }, + + /* 3. Three callsites with arities 1/2/3 in one fn. Pre-fix + * wwstage fatals at the second gather. Post-fix three distinct + * @vararg_d_0/1/2 slots are minted. */ + { "three_arity_drift", + "package main;\n" + "import os;\n" + "fn pick(args: (str | rune)...) i32 = { return args.len; };\n" + "export fn main() i32 = {\n" + " if (pick(\"a\") != 1) { os.exit(11); };\n" + " if (pick(\"a\", \"b\") != 2) { os.exit(12); };\n" + " if (pick(\"a\", \"b\", \"c\") != 3) { os.exit(13); };\n" + " return 0;\n" + "};\n", + 0 }, +}; + +static int +build_with(const char *driver, const char *src_path, const char *tmpdir) +{ + char cmd[1024]; + snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>/dev/null", + tmpdir, driver, src_path); + return runwait(cmd); +} + +static int +exec_bin(const char *bin) +{ + return runwait(bin); +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[512]; + if (bin[0] != '/') { + char cwd[256]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char cdrv[640], wdrv[640]; + 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++) { + const struct row *r = &rows[i]; + + char src[64], tmpdir[64]; + snprintf(src, sizeof src, "/tmp/vararg_seq_%d_%d.ww", + getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/vararg_seq_%d_d_%d", + getpid(), i); + FILE *f = fopen(src, "wb"); + if (!f) { fail++; total++; continue; } + fputs(r->src, f); + fclose(f); + mkdir(tmpdir, 0755); + + 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'; + + total++; + if (build_with(cdrv, src, tmpdir) != 0) { + fprintf(stderr, + "vararg_seq_percall[cs][%s]: build failed\n", + r->label); + fail++; + } else { + int got = exec_bin(outbin); + if (got != r->want) { + fprintf(stderr, + "vararg_seq_percall[cs][%s]: rc=%d want=%d\n", + r->label, got, r->want); + fail++; + } + } + unlink(outbin); + + if (have_ww) { + total++; + if (build_with(wdrv, src, tmpdir) != 0) { + fprintf(stderr, + "vararg_seq_percall[ws][%s]: build failed\n", + r->label); + fail++; + } else { + int got = exec_bin(outbin); + if (got != r->want) { + fprintf(stderr, + "vararg_seq_percall[ws][%s]: rc=%d want=%d\n", + r->label, got, r->want); + fail++; + } + } + unlink(outbin); + } + + unlink(src); + rmdir(tmpdir); + } + + if (fail) { + fprintf(stderr, + "vararg_seq_percall: %d/%d rows failed\n", fail, total); + return 1; + } + printf("vararg_seq_percall: %d/%d ok\n", total, total); + return 0; +}