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

@@ -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;