From 069548d4242fe4d81010d8795236b3b4434fea5b Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 18 May 2026 16:00:31 +0900 Subject: [PATCH] cstage+test: graduate hidden-name mklabel sites to @-prefix SSoT (#26) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Class A frame-layout landmine pre-located; #26c queued for size- strategy convergence per rule 10. Cstage's tagged-scratch sites previously stamped per-call labels via mklabel "tagbase"/"tagscr"/"argscr"/"idxscr", bumping labelseq once per call and allocating a fresh frame slot. Wwstage routes the same sites through localadd("@tagbase", ...) and localadd("@tagscr", c.tagscrsz, nil) — the @-dedup shares ONE slot per name per fn and never touches labelseq. @tagscr is shared across THREE wwstage sites: cgenutil.ww:180 pushargsrev struct-payload widen, cgenutil.ww:2918 cgwidentaggedstore via_outer, cgenexpr.ww:3524 cgindex tagged-element. Worker's initial draft introduced cg_argscr / cg_idxscr as separate cache vars — names that don't exist in wwstage. Per rob's rule-10 amendment those collapsed to a single cg_tagscr shared across the 3 sites, matching wwstage's @tagscr SSoT exactly. Cstage now caches two slots matching wwstage's namespace exactly: cg_tagbase (8B base spill, 1 site at cgwidentaggedstore via_outer) and cg_tagscr (sized scratch shared across the 3 sites above). Eliminates per-call labelseq bumps and per-call frame churn. Class A byte-id drift (silent corpus-coverage-blind landmine) closed for the 1-name shape match. Model: STATUS-3 #15 commit 987391b routed @retscr through the same SSoT via cg_retscr; this commit extends the carve-out to @tagbase and @tagscr. Size strategy: cstage has no scanlocals pre-pass (wwstage's c.tagscrsz pre-pass at cgendecl.ww:32 tagscrbump computes the per-fn max). First call across the 3 @tagscr sites sizes the slot; subsequent calls reuse if sz <= cached, fatal() if larger (rule 7: surface-don't-silently-corrupt). Long-term rule-10 convergence — wwstage DOWN from scanlocals to first-use+fail-loud on BOTH stages (per rob: aligning richer DOWN to leaner) — is filed as #26c, separate concern from #26's name-SSoT graduation. Tests: - 736_cstage_label_ssot succ_rows: pins cstage-vs-wwstage cmp -s byte-id on the canonical pointer-rooted two-tagged-store shape (two `c.v = (...: bag);` writes through *cell). Pre-fix cstage frame was 16B+48B larger (2*@tagbase + 2*@tagscr per call); post-fix single-slot SSoT matches wwstage byte-for-byte. - 736_cstage_label_ssot fail_rows: pre-locates the size-grow landmine. A fn with two unions of different slot sizes (16B then 24B) routed through @tagscr; cstage must fatal() with "@tagscr cached sz" + size mismatch + #26c follow-up cite. Gates corpus growth into this shape against silent miscompile. 110/110 ok. 995_self_rebuild byte-id holds (ww2 == ww3 == ww4). --- Makefile | 5 + cmd/w6c/cgen.c | 92 ++++++++++-- test/wcc/736_cstage_label_ssot.c | 250 +++++++++++++++++++++++++++++++ 3 files changed, 338 insertions(+), 9 deletions(-) create mode 100644 test/wcc/736_cstage_label_ssot.c diff --git a/Makefile b/Makefile index 1a37908a..19e88832 100644 --- a/Makefile +++ b/Makefile @@ -265,6 +265,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_enum_modshadow \ $(BIN)/test_struct_modshadow \ $(BIN)/test_def_modshadow \ + $(BIN)/test_cstage_label_ssot \ $(BIN)/test_fnparams_bare_leaf_shadow \ $(BIN)/test_fnret_bare_leaf_shadow \ $(BIN)/test_param_shadow_mod \ @@ -625,6 +626,10 @@ $(BIN)/test_def_modshadow: test/wcc/735_def_modshadow.c \ $(BIN)/w6c $(BIN)/w6c_ww | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_cstage_label_ssot: test/wcc/736_cstage_label_ssot.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 cd541fb5..c47f0ad5 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -39,6 +39,33 @@ static int *cg_frame; * semantics for synthetic scratches). 0 means "not yet allocated"; * negative offsets returned by local_alloc are the live value. */ static int cg_retscr; +/* Per-fn @-prefix scratch SSoT (task #26, follow-up to #15-cstage's + * @retscr). Pre-#26 each site allocated a labelseq-stamped fresh slot + * per call (mklabel "tagbase" / "tagscr" / "argscr" / "idxscr"); the + * labelseq bumps drifted cstage's ct/ce/end labels ahead of wwstage, + * and the per-call frame growth drifted cstage's framesize ahead too. + * + * Two cached slots match wwstage's `@`-prefix namespace exactly: + * cg_tagbase — 8B base-register spill for cg_widen_tagged_store + * via_outer (mirrors wwstage @tagbase, 1 site). + * cg_tagscr — sized scratch shared across THREE sites: cg_widen_ + * tagged_store via_outer write target, cg_widen_tagged_ + * push struct/tagged-source widen, N_INDEX tagged-element + * assign. Mirrors wwstage @tagscr (cgenutil.ww:180 + + * :2918, cgenexpr.ww:3524). Wwstage shares the slot via + * localadd `@`-prefix dedup, sized to `c.tagscrsz` + * (per-fn max computed by scanlocals pre-pass). + * + * Cstage has no pre-pass: first call across the 3 sites sizes the + * slot; subsequent calls (any of the 3 sites) reuse if sz ≤ cached, + * fatal() if larger. Per rule 7: surface, don't silently corrupt the + * frame. The size-strategy convergence (wwstage DOWN to first-use+ + * fail-loud on BOTH stages, or cstage UP to a scanlocals pre-pass) is + * filed as #26c — separate concern. _sz tracks cached allocation size. */ +static int cg_tagbase; +static int cg_tagbase_sz; +static int cg_tagscr; +static int cg_tagscr_sz; /* System V AMD64 sret discipline (task #23). Plain TY_STRUCT returns * with size > 24B are passed via a hidden first-arg pointer (RDI) to * a caller-prealloc dest; the callee writes through that pointer and @@ -1186,11 +1213,29 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src, int base_spill = 0; int write_off = slot_off; if (via_outer) { - const char *spname = mklabel(c, "tagbase"); - base_spill = local_alloc(c, locals_p, spname, 8, cg_frame); + if (cg_tagbase != 0) { + base_spill = cg_tagbase; + } else { + base_spill = local_alloc(c, locals_p, "@tagbase", 8, + cg_frame); + cg_tagbase = base_spill; + cg_tagbase_sz = 8; + } ins2(c, A_MOVQ, areg(base_reg), amem(D_BP, base_spill)); - const char *scname = mklabel(c, "tagscr"); - write_off = local_alloc(c, locals_p, scname, sz, cg_frame); + if (cg_tagscr != 0) { + if (sz > cg_tagscr_sz) + fatal("cg_widen_tagged_store: @tagscr " + "cached sz %d, need %d (per-fn slot " + "growth needs scanlocals pre-pass — " + "STATUS-4 #26c follow-up)", + cg_tagscr_sz, sz); + write_off = cg_tagscr; + } else { + write_off = local_alloc(c, locals_p, "@tagscr", sz, + cg_frame); + cg_tagscr = write_off; + cg_tagscr_sz = sz; + } /* Pre-zero so str/scalar branches (which leave high words * untouched when sz exceeds the variant's footprint) still * deliver a clean slot to the copy-out. */ @@ -1469,8 +1514,19 @@ cg_widen_tagged_push(Cg *c, Local **locals_p, Type *dst, Node *src, int sz) ins1(c, A_PUSHQ, areg(D_AX)); /* tag at +0 */ return; } - const char *scr_name = mklabel(c, "argscr"); - int scr = local_alloc(c, locals_p, scr_name, sz, cg_frame); + int scr; + if (cg_tagscr != 0) { + if (sz > cg_tagscr_sz) + fatal("cg_widen_tagged_push: @tagscr cached sz %d, " + "need %d (per-fn slot growth needs scanlocals " + "pre-pass — STATUS-4 #26c follow-up)", + cg_tagscr_sz, sz); + scr = cg_tagscr; + } else { + scr = local_alloc(c, locals_p, "@tagscr", sz, cg_frame); + cg_tagscr = scr; + cg_tagscr_sz = sz; + } /* Zero the scratch slot first so any pad word the store path * leaves untouched (struct payload shorter than the slot's value * area) reads as 0 on the callee. The store path then writes the @@ -3443,9 +3499,23 @@ cgexpr(Cg *c, Node *n, Local *locals) * the function frame; no cleanup needed. */ if ((is_arr || is_sl || is_ptr) && elem_tagged) { int ssz = esz; - const char *scrn = mklabel(c, "idxscr"); - int scr = local_alloc(c, &locals, scrn, ssz, - cg_frame); + int scr; + if (cg_tagscr != 0) { + if (ssz > cg_tagscr_sz) + fatal("N_INDEX tagged: " + "@tagscr cached sz %d, " + "need %d (per-fn slot " + "growth needs scanlocals " + "pre-pass — STATUS-4 #26c " + "follow-up)", + cg_tagscr_sz, ssz); + scr = cg_tagscr; + } else { + scr = local_alloc(c, &locals, + "@tagscr", ssz, cg_frame); + cg_tagscr = scr; + cg_tagscr_sz = ssz; + } ins2(c, A_XORQ, areg(D_AX), areg(D_AX)); for (int k = 0; k < ssz; k += 8) ins2(c, A_MOVQ, areg(D_AX), @@ -7180,6 +7250,10 @@ cgfn(Cg *c, FILE *out, Node *fn) nloops = 0; cg_ret_type = fn->type ? fn->type->ret : NULL; cg_retscr = 0; + cg_tagbase = 0; + cg_tagbase_sz = 0; + cg_tagscr = 0; + cg_tagscr_sz = 0; cg_sret_arg_off = 0; cg_sret_dest_off = 0; cg_sretscr_off = 0; diff --git a/test/wcc/736_cstage_label_ssot.c b/test/wcc/736_cstage_label_ssot.c new file mode 100644 index 00000000..7a2c3885 --- /dev/null +++ b/test/wcc/736_cstage_label_ssot.c @@ -0,0 +1,250 @@ +/* + * 736_cstage_label_ssot — sentinel for STATUS-4 #26. Cstage's tagged- + * scratch sites previously stamped per-call labels via mklabel "tagbase" + * / "tagscr" / "argscr" / "idxscr". Each call bumped `labelseq` and + * allocated a fresh frame slot. Wwstage's mirrors use `@`-prefix names + * routed through `localadd` (selfhost/cmd/wcc/cgen.ww) — the `@`-dedup + * shares one slot per name per fn and never touches labelseq. Per-fn + * `@tagbase` is one slot (8B); per-fn `@tagscr` is ONE slot shared + * across cgwidentaggedstore via_outer (cgenutil.ww:2918), pushargsrev + * struct-payload widen (cgenutil.ww:180) and cgindex tagged-element + * (cgenexpr.ww:3524). + * + * Post-#26 cstage caches `cg_tagbase` + `cg_tagscr` (single shared slot + * across the three @tagscr sites — matches wwstage's namespace exactly). + * The size-strategy gap remains: wwstage's scanlocals pre-pass sizes + * `c.tagscrsz` to the per-fn max BEFORE emit; cstage has no pre-pass, + * so first call across the 3 sites sizes the slot and subsequent + * callers must fit. fatal() per rule 7 if growth is needed. Long-term + * convergence (wwstage DOWN to first-use+fail-loud on BOTH stages, or + * cstage UP to a scanlocals pre-pass) is filed as #26c — separate + * concern, not in #26's scope. + * + * Two row families: + * succ_rows — pin cstage vs wwstage asm `cmp -s` byte-id on the + * canonical pointer-rooted two-tagged-store shape. + * Pre-#26 cstage's frame was 16B larger (extra @tagbase + * + @tagscr per call); post-#26 they match. + * fail_rows — pre-locate the Class A landmine: a fn that needs the + * @tagscr slot to grow within one cgfn. Cstage must + * fatal() with the expected pattern (rule 7: surface, + * don't silently corrupt the frame). Tests that the + * next stdlib commit landing into this shape gets a + * loud failure rather than a silent miscompile. + */ +#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; }; + +static const struct row succ_rows[] = { + /* Two struct-pointer-rooted tagged stores in the same fn. Each + * `c.v = (...: bag);` enters cg_widen_tagged_store with a + * non-BP base (D_BX, pointer-rooted dst) which routes through + * the @tagbase spill + @tagscr scratch path. Pre-#26: 2 @tagbase + * slots + 2 @tagscr slots in cstage's frame; wwstage shares one + * @tagbase + one @tagscr via localadd `@`-dedup. Post-#26: cstage + * caches both per-fn via cg_tagbase / cg_tagscr (mirrors + * cg_retscr from #15-cstage) so the second call reuses, matching + * wwstage. */ + { "two_struct_ptr_tagged_stores", + "type pair = struct { hi: i64, lo: i64 };\n" + "type bag = (pair | i64);\n" + "type cell = struct { v: bag };\n" + "export fn run(c: *cell, p: pair, q: pair) i64 = {\n" + " c.v = (p: bag);\n" + " c.v = (q: bag);\n" + " return 0i64;\n" + "};\n" }, +}; + +struct failrow { + const char *label; + const char *src; + const char *want_pattern; /* substring expected in cstage stderr */ +}; + +static const struct failrow fail_rows[] = { + /* Pre-located landmine: two pointer-rooted tagged stores into + * differently-sized union slots within ONE fn. First store pins + * cg_tagscr_sz to the smaller slot; second store needs a larger + * slot than cached → fatal() with the size-grow pattern. Wwstage + * handles this via scanlocals pre-pass (c.tagscrsz = max). Cstage + * has no pre-pass — fail-loud per rule 7 keeps the next stdlib + * commit hitting this shape from silently corrupting the frame. + * + * Slot sizes: (i64 | i32) → 16B (tag + max(8,4) → 8+8). (i64 | str) + * → 24B (tag + max(8, str=16) → 8+16). First write 16B, second + * 24B; cstage must fatal on the second call. */ + { "tagscr_size_grow_fatal", + "type small = (i64 | i32);\n" + "type big = (i64 | str);\n" + "type cell_a = struct { v: small };\n" + "type cell_b = struct { v: big };\n" + "export fn run(a: *cell_a, b: *cell_b, x: i64, s: str) i64 = {\n" + " a.v = (x: small);\n" + " b.v = (s: big);\n" + " return 0i64;\n" + "};\n", + "@tagscr cached sz" }, +}; + +static int +emit_s(const char *w6c, const char *src_text, int i, char *out_s, size_t cap) +{ + char src[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/lbl_ssot_%d_%d.ww", getpid(), i); + snprintf(out_s, cap, "/tmp/lbl_ssot_%d_%d_%s.s", + getpid(), i, w6c[strlen(w6c) - 1] == 'w' ? "ww" : "c"); + + 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; +} + +/* Run w6c with stderr captured to a tmp file. Returns rc and writes + * stderr path into err_out. Caller unlinks. */ +static int +emit_capture_stderr(const char *w6c, const char *src_text, int i, + char *err_out, size_t cap) +{ + char src[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/lbl_ssot_fail_%d_%d.ww", getpid(), i); + snprintf(err_out, cap, "/tmp/lbl_ssot_fail_%d_%d.err", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(src_text, f); + fclose(f); + + snprintf(cmd, sizeof cmd, "%s -o /dev/null %s 2>%s", w6c, src, err_out); + int rc = runwait(cmd); + unlink(src); + return rc; +} + +static int +stderr_contains(const char *path, const char *needle) +{ + FILE *f = fopen(path, "rb"); + if (!f) return 0; + char buf[4096]; + size_t n = fread(buf, 1, sizeof buf - 1, f); + fclose(f); + buf[n] = '\0'; + return strstr(buf, needle) != NULL; +} + +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; + + int nsucc = (int)(sizeof succ_rows / sizeof succ_rows[0]); + for (int i = 0; i < nsucc; i++) { + char cs_path[128], ws_path[128]; + + if (emit_s(w6c, succ_rows[i].src, i, cs_path, sizeof cs_path) != 0) { + fprintf(stderr, + "cstage_label_ssot[cstage][%s]: w6c failed\n", + succ_rows[i].label); + fail++; total++; continue; + } + total++; + + if (!have_ww) { unlink(cs_path); continue; } + + if (emit_s(w6c_ww, succ_rows[i].src, i, ws_path, sizeof ws_path) != 0) { + fprintf(stderr, + "cstage_label_ssot[wwstage][%s]: w6c_ww failed\n", + succ_rows[i].label); + fail++; total++; + unlink(cs_path); + continue; + } + total++; + + /* Byte-id between stages — the fix-pin. Pre-fix cstage's + * frame was 16B larger and the second-store offsets pointed + * at extra slots; post-fix they match. */ + total++; + char cmd[512]; + snprintf(cmd, sizeof cmd, "cmp -s %s %s", cs_path, ws_path); + if (runwait(cmd) != 0) { + fprintf(stderr, + "cstage_label_ssot[%s]: cstage vs wwstage asm differs\n", + succ_rows[i].label); + fail++; + } + + unlink(cs_path); unlink(ws_path); + } + + int nfail = (int)(sizeof fail_rows / sizeof fail_rows[0]); + for (int i = 0; i < nfail; i++) { + char err_path[128]; + int rc = emit_capture_stderr(w6c, fail_rows[i].src, i, + err_path, sizeof err_path); + total++; + if (rc == 0) { + fprintf(stderr, + "cstage_label_ssot[fail][%s]: expected cstage " + "fatal, got success exit\n", fail_rows[i].label); + fail++; + unlink(err_path); + continue; + } + total++; + if (!stderr_contains(err_path, fail_rows[i].want_pattern)) { + fprintf(stderr, + "cstage_label_ssot[fail][%s]: stderr missing " + "pattern \"%s\"\n", + fail_rows[i].label, fail_rows[i].want_pattern); + fail++; + } + unlink(err_path); + } + + if (fail) { + fprintf(stderr, + "cstage_label_ssot: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("cstage_label_ssot: %d/%d ok\n", total, total); + return 0; +}