cstage+test: graduate hidden-name mklabel sites to @-prefix SSoT (#26)

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).
This commit is contained in:
2026-05-18 16:00:31 +09:00
parent a8d1df6090
commit 069548d424
3 changed files with 338 additions and 9 deletions

View File

@@ -265,6 +265,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
$(BIN)/test_enum_modshadow \ $(BIN)/test_enum_modshadow \
$(BIN)/test_struct_modshadow \ $(BIN)/test_struct_modshadow \
$(BIN)/test_def_modshadow \ $(BIN)/test_def_modshadow \
$(BIN)/test_cstage_label_ssot \
$(BIN)/test_fnparams_bare_leaf_shadow \ $(BIN)/test_fnparams_bare_leaf_shadow \
$(BIN)/test_fnret_bare_leaf_shadow \ $(BIN)/test_fnret_bare_leaf_shadow \
$(BIN)/test_param_shadow_mod \ $(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) $(BIN)/w6c $(BIN)/w6c_ww | $(BIN)
$(CC) $(CFLAGS) -o $@ $< $(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)/test_fnparams_bare_leaf_shadow: test/wcc/732_fnparams_bare_leaf_shadow.c \
$(BIN)/w6c $(BIN)/w6c_ww | $(BIN) $(BIN)/w6c $(BIN)/w6c_ww | $(BIN)
$(CC) $(CFLAGS) -o $@ $< $(CC) $(CFLAGS) -o $@ $<

View File

@@ -39,6 +39,33 @@ static int *cg_frame;
* semantics for synthetic scratches). 0 means "not yet allocated"; * semantics for synthetic scratches). 0 means "not yet allocated";
* negative offsets returned by local_alloc are the live value. */ * negative offsets returned by local_alloc are the live value. */
static int cg_retscr; 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 /* System V AMD64 sret discipline (task #23). Plain TY_STRUCT returns
* with size > 24B are passed via a hidden first-arg pointer (RDI) to * with size > 24B are passed via a hidden first-arg pointer (RDI) to
* a caller-prealloc dest; the callee writes through that pointer and * 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 base_spill = 0;
int write_off = slot_off; int write_off = slot_off;
if (via_outer) { if (via_outer) {
const char *spname = mklabel(c, "tagbase"); if (cg_tagbase != 0) {
base_spill = local_alloc(c, locals_p, spname, 8, cg_frame); 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)); ins2(c, A_MOVQ, areg(base_reg), amem(D_BP, base_spill));
const char *scname = mklabel(c, "tagscr"); if (cg_tagscr != 0) {
write_off = local_alloc(c, locals_p, scname, sz, cg_frame); 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 /* Pre-zero so str/scalar branches (which leave high words
* untouched when sz exceeds the variant's footprint) still * untouched when sz exceeds the variant's footprint) still
* deliver a clean slot to the copy-out. */ * 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 */ ins1(c, A_PUSHQ, areg(D_AX)); /* tag at +0 */
return; return;
} }
const char *scr_name = mklabel(c, "argscr"); int scr;
int scr = local_alloc(c, locals_p, scr_name, sz, cg_frame); 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 /* Zero the scratch slot first so any pad word the store path
* leaves untouched (struct payload shorter than the slot's value * leaves untouched (struct payload shorter than the slot's value
* area) reads as 0 on the callee. The store path then writes the * 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. */ * the function frame; no cleanup needed. */
if ((is_arr || is_sl || is_ptr) && elem_tagged) { if ((is_arr || is_sl || is_ptr) && elem_tagged) {
int ssz = esz; int ssz = esz;
const char *scrn = mklabel(c, "idxscr"); int scr;
int scr = local_alloc(c, &locals, scrn, ssz, if (cg_tagscr != 0) {
cg_frame); 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)); ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
for (int k = 0; k < ssz; k += 8) for (int k = 0; k < ssz; k += 8)
ins2(c, A_MOVQ, areg(D_AX), ins2(c, A_MOVQ, areg(D_AX),
@@ -7180,6 +7250,10 @@ cgfn(Cg *c, FILE *out, Node *fn)
nloops = 0; nloops = 0;
cg_ret_type = fn->type ? fn->type->ret : NULL; cg_ret_type = fn->type ? fn->type->ret : NULL;
cg_retscr = 0; cg_retscr = 0;
cg_tagbase = 0;
cg_tagbase_sz = 0;
cg_tagscr = 0;
cg_tagscr_sz = 0;
cg_sret_arg_off = 0; cg_sret_arg_off = 0;
cg_sret_dest_off = 0; cg_sret_dest_off = 0;
cg_sretscr_off = 0; cg_sretscr_off = 0;

View File

@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
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;
}