selfhost+cstage+test: graduate frame growth to first-use+fail-loud (#15)

Subsumes #36. Drop wwstage scanlocals pre-pass; both stages converge on
first-use+fail-loud frame growth, rule-10 polarity DOWN to leaner side.
#36's surfaces (frame-total divergence on match-arm case-let; sibling
offset divergence in variadic+iter+match-prev compositions) close
naturally — running-max c.frame includes every first-use binding.

selfhost/cmd/wcc: add atlocals persistent @-prefix registry surviving
cgblock save/restore; add cgoutbuf/cgoutmode/cgout_enable/disable/flush
for deferred prologue (emit body to buffer, finalise c.frame, then
TEXT/SUBQ + flush); localadd @-prefix dedups against atlocals +
fail-louds on size-grow (rule 7 — no silent truncate); cgreturn-tagged
routes through @retscr (was colliding with @tagscr on arg-widen sizes);
variadic gather esz uses raw primsize (rune->4) not slotsize (rune->8)
— matches cstage and fixes the #36 sibling runtime miscompile in
non-leaf variadic+iter+match-prev callees.

cmd/w6c/cgen.c: drop the over-allocation hack ("for byte-id with
wwstage scanlocals reservation") since wwstage no longer over-reserves;
add fail-loud on @sretscr size-grow; @tagscr sites pass actual slot_sz
instead of stale c.tagscrsz.

748_size_strategy_convergence: table-driven 4 rows x 2 stages
(tag_variadic_runearm, trim_iter_match_prev, variadic_gather_rune_stride,
leaf_baseline). Each exercises a #36 surface shape; 8/8 ok.

Net -1565 lines. Sister latents filed as cosmetic (cs/ws frame size
drift on multiple-variadic-call fns): labelseq drift + varargseq
stuck at 0 — both bootstrap-byte-id safe (ww2==ww3==ww4 holds since
both ww2 and ww3 are wwstage outputs).

make test 122/122; ww2==ww3==ww4 byte-id holds via 995_self_rebuild.
This commit is contained in:
2026-05-19 02:13:58 +09:00
parent 7a278c1a2d
commit 5609d0456f
10 changed files with 1197 additions and 2469 deletions

View File

@@ -497,7 +497,7 @@ fn cgstrlit(c: *cgen, n: *node) void = {
let nstr: str = n.str;
let lab: str = internstrlit(c, nstr);
emitline("\tLEAQ\t");
os.write(1, lab.ptr, lab.len: u64);
emitbytes( lab.ptr, lab.len: u64);
emitline("(SB), AX\n");
emitline("\tMOVQ\t$");
emitint(nstr.len: i64);
@@ -564,7 +564,7 @@ fn cgident(c: *cgen, n: *node) void = {
let bytes: str = drhs.str;
let lab: str = internstrlit(c, bytes);
emitline("\tLEAQ\t");
os.write(1, lab.ptr, lab.len: u64);
emitbytes( lab.ptr, lab.len: u64);
emitline("(SB), AX\n");
emitline("\tMOVQ\t$");
emitint(bytes.len: i64);
@@ -1008,9 +1008,9 @@ fn cgmatch(c: *cgen, n: *node) void = {
// (N_INDEX) and tagged-field loads (N_DOT, fixed by
// #28) produce the same triple. Nullable returns are
// single-word (AX = ptr); only +0 is read.
// Scrutinee type + spill size factored into matchscrutt
// / matchspillsz so scanlocals stays lockstep — see
// cgenutil.ww (task #9 align-down to cstage).
// Scrutinee type + spill size resolved through matchscrutt
// / matchspillsz at first use (#15) — see cgenutil.ww
// (task #9 align-down to cstage).
scrutt = matchscrutt(c, scrut);
let spillsz: i32 = matchspillsz(c, scrutt);
scrutoff = localalloc(c, "@match_spill", spillsz, nil);
@@ -1520,7 +1520,7 @@ fn cgdot(c: *cgen, n: *node) void = {
if (streq(fld, "ptr")) {
let lab: str = internstrlit(c, bytes);
emitline("\tLEAQ\t");
os.write(1, lab.ptr, lab.len: u64);
emitbytes( lab.ptr, lab.len: u64);
emitline("(SB), AX\n");
return;
};
@@ -1754,7 +1754,7 @@ fn cgdot(c: *cgen, n: *node) void = {
let bytes: str = drhs.str;
let lab: str = internstrlit(c, bytes);
emitline("\tLEAQ\t");
os.write(1, lab.ptr, lab.len: u64);
emitbytes( lab.ptr, lab.len: u64);
emitline("(SB), AX\n");
emitline("\tMOVQ\t$");
emitint(bytes.len: i64);
@@ -2941,7 +2941,7 @@ fn cgcall(c: *cgen, n: *node) void = {
// 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 scanlocals stamped on n.uval.
// matches the one cgcall stamped on n.uval at first emit.
{
let nfixed_v: i32 = 0;
let varp: *node = callee_variadic_param(c, callee, &nfixed_v);
@@ -2982,7 +2982,25 @@ fn cgcall(c: *cgen, n: *node) void = {
let seq: i32 = n.uval: i32;
let dname: str = mkvarargname(c, "@vararg_d_", seq);
let sname: str = mkvarargname(c, "@vararg_sl_", seq);
let esz: i32 = slotsize(c, varp.lhs);
// Use raw element size, not stack-padded
// slotsize. cstage cmd/w6c/cgen.c cgcall
// gathers a `T...` slice at velem->size stride
// (MOVL for u32, MOVB for u8); the callee
// `arg[i]` reads at the same raw stride. wwstage
// previously sized through slotsize which pads
// scalars to 8, mismatching the stride at the
// callee read site — runtime miscompile in
// `(rune...)` callees per #36.
let esz: i32 = 8;
if (varp.lhs != nil) {
if (varp.lhs.kind == nkind.N_TNAME) {
let ps: i32 = primsize(varp.lhs.str);
if (ps > 0) { esz = ps; }
else { esz = slotsize(c, varp.lhs); };
} else {
esz = slotsize(c, varp.lhs);
};
};
if (esz < 1) { esz = 1; };
let velemtagged: bool = istaggedtype(c, varp.lhs);
let velemstr: bool = isstrtype(c, varp.lhs);
@@ -3079,7 +3097,7 @@ fn cgcall(c: *cgen, n: *node) void = {
// pops have finished (so they don't clobber RDI). The dest off
// is either the receive site's slot (c.sretdestoff, propagated
// from cglet / cgassign ident) or the per-fn @sretscr discard
// slot reserved by scanlocals.
// slot, sized at first use per #15/#26c.
let sretcs: i32 = callsretsize(c, n);
let sretcalloff: i32 = 0;
if (sretcs > 0) {
@@ -3088,7 +3106,7 @@ fn cgcall(c: *cgen, n: *node) void = {
c.sretdestoff = 0;
} else {
sretcalloff = localadd(c, "@sretscr",
c.sretscrsz, nil);
sretcs, nil);
};
};
// Pop forward. Float args were pushed as 8 bytes from X0 via
@@ -3242,8 +3260,9 @@ fn cgcall(c: *cgen, n: *node) void = {
// on the forwarding branch.
if (sretcs > 0) {
if (c.sretforward != 0) {
let sretargoff: i32 = localfind(c, "@sretarg");
emitline("\tMOVQ\t");
emitoff(c.sretargoff: i64);
emitoff(sretargoff: i64);
emitline("(BP), DI\n");
c.sretforward = 0;
} else {
@@ -3608,12 +3627,12 @@ fn cgassign(c: *cgen, n: *node) void = {
// str / scalar / subset / nullable variants uniformly),
// then compute &arr[i] and byte-copy. The scratch
// (@tagscr) is reused across all tagged-arr stores in
// the function and counted once in scanlocals.
// the function; first-use sizes the slot (#15/#26c).
if (elemtn != nil) {
if (istaggedtype(c, elemtn)) {
let slot_sz: i32 = slotsize(c, elemtn);
let scroff: i32 = localadd(c, "@tagscr",
c.tagscrsz, nil);
slot_sz, nil);
// Pre-zero scratch (matches push helper).
emitline("\tXORQ\tAX, AX\n");
let zz: i32 = 0;