From 3bd9b1d56f2e284cefcb4100ecca720ee64c74ba Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 18 May 2026 21:13:42 +0900 Subject: [PATCH] cstage+test: store .len/.cap on every variadic-pack element (#16) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cstage variadic gather stored only AX (.ptr) per element; .len and .cap read stack residue at the callee. Tagged-union variadic path escaped because cg_widen_tagged_store wrote the full slot — but primitive-type variadics (str..., slice...) silently dropped the trailing fields. Selfhost only uses tagged-union variadics (formattable...) so bootstrap byte-id ww2==ww3==ww4 stayed green; the bug surfaced in worker-strings pre-flight (session 5) on the Hare-faithful concat(strs: str...) shape. Per-element store branch now mirrors selfhost/cmd/wcc/cgenexpr.ww velemstr (AX→slot+0, BX→slot+8) and velemslice (AX→slot+0, BX→slot+8, CX→slot+16). Also swap dname-before-sname allocation order in the variadic-pack frame layout to match wwstage scanlocals + localadd order (cgendecl.ww:507-516 and cgenexpr.ww:2949-2954); without the swap post-fix asm has correct stores at mismatched offsets vs wwstage. Rule-10 alignment: cstage UP to wwstage's already-correct primitive variadic path. 743_variadic_pack pins the contract: asm-presence ≥3 ptr-stores + ≥3 len-stores in caller TEXT on both stages, plus cs-vs-ws cmp -s byte-id per row. 117/117 ok. Bootstrap byte-id ww2==ww3==ww4 holds. Unblocks: lib/bytes contains-variadic, lib/strings sub variadic, and the concat/trim/contains family that c1 shipped non-variadic. --- Makefile | 5 + cmd/w6c/cgen.c | 36 ++++++- test/wcc/743_variadic_pack.c | 188 +++++++++++++++++++++++++++++++++++ 3 files changed, 226 insertions(+), 3 deletions(-) create mode 100644 test/wcc/743_variadic_pack.c diff --git a/Makefile b/Makefile index 01d68dea..360ae10c 100644 --- a/Makefile +++ b/Makefile @@ -275,6 +275,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_chained_write \ $(BIN)/test_dotbase_chained \ $(BIN)/test_parse_error \ + $(BIN)/test_variadic_pack \ $(BIN)/test_fnparams_bare_leaf_shadow \ $(BIN)/test_fnret_bare_leaf_shadow \ $(BIN)/test_param_shadow_mod \ @@ -664,6 +665,10 @@ $(BIN)/test_parse_error: test/wcc/742_parse_error.c \ $(BIN)/w6c $(BIN)/w6c_ww | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_variadic_pack: test/wcc/743_variadic_pack.c \ + $(BIN)/w6c $(BIN)/w6c_ww | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_fnparams_bare_leaf_shadow: test/wcc/732_fnparams_bare_leaf_shadow.c \ $(BIN)/w6c $(BIN)/w6c_ww | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index c47f0ad5..11d64eac 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -4302,16 +4302,24 @@ cgexpr(Cg *c, Node *n, Local *locals) ? vsu->sub : NULL; int esz = (velem && velem->size) ? (int)velem->size : 8; - const char *slname = mklabel(c, "vararg_sl"); - int sloff = localoff(c, &locals, - slname, 24, cg_frame); + /* Allocate dname BEFORE sname so the + * descriptor lives below the element + * buffer, matching the wwstage scanlocals + * reservation order (rule 10). */ int doff = 0; if (nvar > 0) { const char *dname = mklabel(c, "vararg_d"); doff = localoff(c, &locals, dname, nvar * esz, cg_frame); + } + const char *slname = mklabel(c, "vararg_sl"); + int sloff = localoff(c, &locals, + slname, 24, cg_frame); + if (nvar > 0) { int v_is_tagged = velem && tagged_arg_size(velem) > 0; + int v_is_str = type_isstr(velem); + int v_is_slice = type_isslice(velem); for (int j = 0; j < nvar; j++) { Node *a = args[nfixed + j]; int slot = doff + j * esz; @@ -4322,6 +4330,28 @@ cgexpr(Cg *c, Node *n, Local *locals) continue; } cgexpr(c, a, locals); + /* str / slice element: cgexpr + * returns the full descriptor in + * AX/(BX)/(CX); a bare MOVQ AX + * stores .ptr only and the + * trailing fields read stack + * garbage at the callee. */ + if (v_is_str) { + ins2(c, A_MOVQ, areg(D_AX), + amem(D_BP, slot)); + ins2(c, A_MOVQ, areg(D_BX), + amem(D_BP, slot + 8)); + continue; + } + if (v_is_slice) { + ins2(c, A_MOVQ, areg(D_AX), + amem(D_BP, slot)); + ins2(c, A_MOVQ, areg(D_BX), + amem(D_BP, slot + 8)); + ins2(c, A_MOVQ, areg(D_CX), + amem(D_BP, slot + 16)); + continue; + } int op = A_MOVQ; if (esz == 1) op = A_MOVB; else if (esz == 4) op = A_MOVL; diff --git a/test/wcc/743_variadic_pack.c b/test/wcc/743_variadic_pack.c new file mode 100644 index 00000000..b9e4c4fa --- /dev/null +++ b/test/wcc/743_variadic_pack.c @@ -0,0 +1,188 @@ +/* + * 743_variadic_pack — sentinel for STATUS-3 #16. cstage cgen's + * variadic gather (`f(args: str...)` called as `f("a", "b", "c")`) + * emitted only the ptr eightbyte of each str element; the .len + * eightbyte was never stored, so the callee's `args[i].len` read + * stack residue. Tagged-union variadics escaped because they took + * the cg_widen_tagged_store branch; primitive-type variadics + * (str..., slice..., rune... bigger than 8B) did not. + * + * Surfaced by worker-strings pre-flight on the Hare-faithful + * `concat(strs: str...)` shape; no in-tree caller exercised it + * because the c1 strings subset shipped non-variadic. Bootstrap + * byte-id masked it (selfhost only calls fmt.println, which uses + * tagged-union variadics, hitting the widen-store branch). + * + * Rule 10: both stages must emit the same {ptr-store, len-store} + * pair sequence. Pre-fix cstage emitted 3 ptr-stores only; wwstage + * already emitted both halves via cgenexpr.ww's velemstr branch. + * + * Rows pin asm-presence in both stages and cmp -s 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; +} + +static int +slurp(const char *path, char *buf, size_t cap) +{ + FILE *f = fopen(path, "rb"); + if (!f) return -1; + size_t n = fread(buf, 1, cap - 1, f); + fclose(f); + buf[n] = '\0'; + return (int)n; +} + +/* Count occurrences of `needle` in `hay` between [start, end). */ +static int +count_substr(const char *hay, const char *start, const char *end, + const char *needle) +{ + int n = 0; + size_t nlen = strlen(needle); + const char *p = start; + while (p + nlen <= end) { + if (memcmp(p, needle, nlen) == 0) { n++; p += nlen; } + else { p++; } + } + return n; +} + +static int +check_caller_stores(const char *spath) +{ + char buf[1 << 16]; + if (slurp(spath, buf, sizeof buf) < 0) return -1; + + const char *body = strstr(buf, "TEXT main"); + if (!body) { + fprintf(stderr, "variadic_pack: no TEXT main label\n"); + return -1; + } + const char *end = strstr(body, "\nTEXT "); + if (!end) end = buf + strlen(buf); + + /* Three str elements => 3 ptr-stores (MOVQ AX, off(BP)) AND + * 3 len-stores (MOVQ BX, off(BP)). The len-store is the bug + * pin: pre-fix it was missing entirely. */ + int ptr_stores = count_substr(buf, body, end, "MOVQ\tAX, -"); + int len_stores = count_substr(buf, body, end, "MOVQ\tBX, -"); + if (len_stores < 3) { + fprintf(stderr, + "variadic_pack: only %d len-stores (MOVQ BX, -K(BP)) " + "in main; expected >= 3\n", len_stores); + return -1; + } + if (ptr_stores < 3) { + fprintf(stderr, + "variadic_pack: only %d ptr-stores in main; " + "expected >= 3\n", ptr_stores); + return -1; + } + return 0; +} + +static const char *src_text = + "fn sumlen(parts: str...) i32 = {\n" + " let z: i32 = 0;\n" + " let i: i32 = 0;\n" + " for (i < parts.len) {\n" + " z += parts[i].len;\n" + " i += 1;\n" + " };\n" + " return z;\n" + "};\n" + "fn main() i32 = {\n" + " return sumlen(\"a\", \"bb\", \"ccc\");\n" + "};\n"; + +static int +emit_s(const char *w6c, char *out_s, size_t cap, int tag) +{ + char src[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/vp_asm_%d_%d.ww", getpid(), tag); + snprintf(out_s, cap, "/tmp/vp_asm_%d_%d.s", getpid(), tag); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(src_text, f); + fclose(f); + + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, out_s, src); + int rc = runwait(cmd); + unlink(src); + return rc; +} + +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 w6c[640], w6c_ww[640]; + snprintf(w6c, sizeof w6c, "%s/w6c", bin); + snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); + + int have_ww = (access(w6c_ww, X_OK) == 0); + int total = 0, fail = 0; + + char cs_path[128], ws_path[128]; + if (emit_s(w6c, cs_path, sizeof cs_path, 0) != 0) { + fprintf(stderr, "variadic_pack[cstage]: w6c failed\n"); + return 1; + } + total++; + if (check_caller_stores(cs_path) != 0) fail++; + + if (have_ww) { + if (emit_s(w6c_ww, ws_path, sizeof ws_path, 1) != 0) { + fprintf(stderr, + "variadic_pack[wwstage]: w6c_ww failed\n"); + unlink(cs_path); + return 1; + } + total++; + if (check_caller_stores(ws_path) != 0) fail++; + + /* Byte-id between stages on the variadic-pack shape. */ + total++; + char cmd[512]; + snprintf(cmd, sizeof cmd, "cmp -s %s %s", cs_path, ws_path); + if (runwait(cmd) != 0) { + fprintf(stderr, + "variadic_pack: cstage vs wwstage asm differs\n"); + fail++; + } + unlink(ws_path); + } + unlink(cs_path); + + if (fail) { + fprintf(stderr, + "variadic_pack: %d/%d checks failed\n", fail, total); + return 1; + } + printf("variadic_pack: %d/%d ok\n", total, total); + return 0; +}