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

@@ -282,6 +282,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
$(BIN)/test_fnret34_modshadow \
$(BIN)/test_strdef_inline \
$(BIN)/test_def_modqual_modshadow \
$(BIN)/test_size_strategy_convergence \
$(BIN)/test_param_shadow_mod \
$(BIN)/test_localoff_scope \
$(BIN)/test_cast_enum_movl \
@@ -699,6 +700,12 @@ $(BIN)/test_def_modqual_modshadow: test/wcc/747_def_modqual_modshadow.c \
$(BIN)/w6c $(BIN)/w6c_ww | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
$(BIN)/test_size_strategy_convergence: test/wcc/748_size_strategy_convergence.c \
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
$(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \
$(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
$(BIN)/test_match_4arm_cross_module_run: test/wcc/929_match_4arm_cross_module_run.c \
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
$(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \

View File

@@ -51,17 +51,15 @@ static int cg_retscr;
* 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).
* assign. Mirrors wwstage @tagscr — wwstage shares the
* slot via localadd `@`-prefix dedup against c.atlocals.
*
* 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. */
* Both stages now size at first use and fatal() if a later site asks
* for more (rule 7: surface, don't silently corrupt the frame —
* pinned offset can't grow in place once neighbours are allocated).
* Per-fn convergence completed by #15 (#26c follow-up): wwstage
* dropped its scanlocals pre-pass and aligned DOWN to cstage's
* first-use shape. _sz tracks cached allocation size. */
static int cg_tagbase;
static int cg_tagbase_sz;
static int cg_tagscr;
@@ -1239,9 +1237,8 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src,
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)",
"cached sz %d, need %d (pinned offset "
"can't grow in place; rule 7 — #15/#26c)",
cg_tagscr_sz, sz);
write_off = cg_tagscr;
} else {
@@ -1532,8 +1529,8 @@ cg_widen_tagged_push(Cg *c, Local **locals_p, Type *dst, Node *src, int sz)
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)",
"need %d (pinned offset can't grow in place; "
"rule 7 — #15/#26c)",
cg_tagscr_sz, sz);
scr = cg_tagscr;
} else {
@@ -3518,10 +3515,9 @@ cgexpr(Cg *c, Node *n, Local *locals)
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)",
"need %d (pinned offset "
"can't grow in place; "
"rule 7 — #15/#26c)",
cg_tagscr_sz, ssz);
scr = cg_tagscr;
} else {
@@ -4318,8 +4314,8 @@ cgexpr(Cg *c, Node *n, Local *locals)
? (int)velem->size : 8;
/* Allocate dname BEFORE sname so the
* descriptor lives below the element
* buffer, matching the wwstage scanlocals
* reservation order (rule 10). */
* buffer, matching wwstage's emit-time
* order (rule 10). */
int doff = 0;
if (nvar > 0) {
const char *dname = mklabel(c, "vararg_d");
@@ -4612,21 +4608,31 @@ cgexpr(Cg *c, Node *n, Local *locals)
sret_call_sz = cg_sret_retsize(ret);
}
if (sret_call_sz > 0) {
/* Always pre-allocate @sretscr at the first sret CALL
* regardless of whether cg_sret_dest_off is set — keeps
* cstage's frame in lockstep with wwstage's
* scanlocals-based reservation. Single-slot SSoT (cg_
* sretscr_off) mirrors @retscr / @tagscr conventions. */
if (cg_sretscr_off == 0) {
cg_sretscr_off = local_alloc(c,
&locals, "@sretscr",
sret_call_sz, cg_frame);
cg_sretscr_sz = sret_call_sz;
}
/* @sretscr is only needed when the result is dropped
* (no `let x = f();` receiver wired the call's dest into
* cg_sret_dest_off). Allocate first-use per #15/#26c
* size-strategy convergence — wwstage's scanlocals pre-
* pass that used to reserve this slot unconditionally is
* gone; cstage matches by skipping the allocation when a
* dest is already wired. fatal() on a later sret CALL
* needing a bigger slot (rule 7 — pinned offset can't
* grow in place). */
if (cg_sret_dest_off != 0) {
sret_call_off = cg_sret_dest_off;
cg_sret_dest_off = 0;
} else {
if (cg_sretscr_off == 0) {
cg_sretscr_off = local_alloc(c,
&locals, "@sretscr",
sret_call_sz, cg_frame);
cg_sretscr_sz = sret_call_sz;
} else if (sret_call_sz > cg_sretscr_sz) {
fatal("cgcall: @sretscr cached sz "
"%d, need %d (per-fn slot growth "
"post-#15 — pinned offset can't "
"grow in place)",
cg_sretscr_sz, sret_call_sz);
}
sret_call_off = cg_sretscr_off;
}
}
@@ -4708,9 +4714,9 @@ cgexpr(Cg *c, Node *n, Local *locals)
* forwards through an sret callee, source RDI from outer's
* saved @sretarg — inner writes directly into outer's
* caller-prealloc dest. No temporary in outer's frame.
* The @sretscr slot was still allocated above for byte-id
* lockstep with wwstage's scanlocals reservation; it goes
* unused on the forwarding branch. */
* Post-#15 @sretscr is skipped entirely on the forwarding
* branch (no allocation, no frame growth) — earlier scan-
* lockstep reservation is gone. */
if (sret_call_sz > 0) {
if (cg_sret_forward) {
ins2(c, A_MOVQ,
@@ -6768,19 +6774,19 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
if (cg_retscr != 0) {
scr = cg_retscr;
} else {
/* STATUS-3 #15: use the fixed
* "@retscr" SSoT name (mirrors
* wwstage's c.retscroff pattern in
* selfhost/cmd/wcc/cgen.ww). Pre-
* fix mklabel(c, "retscr") consumed
/* Fixed "@retscr" SSoT name —
* mirrors wwstage's localadd
* @-prefix dedup. Pre-fix
* mklabel(c, "retscr") consumed
* one labelseq counter slot per
* function with a tagged return,
* pushing every subsequent ct/ce/
* else/end label 1 ahead of wwstage.
* Site 1 sentinel masked by latent
* struct-widen offset divergence
* (STATUS-3 #20/#21); fix is
* preventive symmetry per rule 10. */
* else/end label 1 ahead of
* wwstage. Site 1 sentinel
* masked by latent struct-widen
* offset divergence (#20/#21);
* fix is preventive symmetry per
* rule 10. */
scr = local_alloc(c, locals,
"@retscr", sz, cg_frame);
cg_retscr = scr;
@@ -6936,9 +6942,9 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
|| n->lhs->kind == N_STRUCTLIT)) {
int sz = (int)rt->size;
/* Single-slot @retscr (#14): see tagged arm
* above for rationale. STATUS-3 #15: fixed
* "@retscr" name avoids bumping labelseq;
* mirrors wwstage's c.retscroff SSoT. */
* above for rationale. Fixed "@retscr" name
* avoids bumping labelseq; mirrors wwstage's
* localadd @-prefix dedup. */
int scr;
if (cg_retscr != 0) {
scr = cg_retscr;

File diff suppressed because it is too large Load Diff

View File

@@ -377,6 +377,13 @@ type structinfo = struct {
type local = struct {
name: str,
off: i32,
sz: i32, // allocated slot size; carried so @-prefix reuse can
// fail-loud (rule 7) if a later site needs a larger
// slot than the first allocation pinned. Per #15/#26c
// size-strategy convergence — wwstage dropped its
// scanlocals pre-pass, so @tagscr/@retscr/@sretscr/
// @tagbase are sized at first-use; subsequent uses
// must fit.
tnode: *node, // declared type expr (nkind.N_TNAME / nkind.N_TPTR / ...) or nil
lnext: *local,
};
@@ -422,6 +429,17 @@ def DEFER_MAX: i32 = 16;
type cgen = struct {
a: *arena,
locals: *local,
// atlocals — persistent registry of `@`-prefix scratch slots
// for the current fn. cgblock save/restores c.locals to scope
// inner shadows (post-#27); a return/cgindex/cgwidentaggedstore
// inside one block must not reallocate @retscr/@tagscr when a
// sibling block uses them again. cgblock leaves atlocals alone
// so the slot offsets survive. localadd checks here first for
// @-prefix names; localfind falls back here when c.locals misses
// an @-name. Pre-#15 this was a handful of named offsets on the
// cgen (c.retscroff / c.sretargoff / c.sretscroff); post-#15
// every @-name flows through the same registry.
atlocals: *local,
frame: i32,
lastwasreturn: i32,
labelseq: i32,
@@ -451,55 +469,34 @@ type cgen = struct {
yieldbuf: *str, // stack of match end labels for yield
defertop: i32,
deferbuf: **node, // stack of deferred exprs (LIFO at return)
// Variadic-call gather state. scanlocals walks the body in pre-
// order DFS and assigns per-call scratch names `@vararg_d_N` /
// `@vararg_sl_N` using this counter; cgcall resets and walks in
// the same order so the names line up at emission time.
// Variadic-call gather state. cgcall assigns per-call scratch
// names `@vararg_d_N` / `@vararg_sl_N` using this counter;
// post #15 the seq is bumped only at emit time so a single
// sequence is observed (the scanlocals pre-pass was dropped).
varargseq: i32,
// Max @tagscr slot_sz across all reservation sites in the current
// function. scanlocals bumps; every emit-time `localadd("@tagscr",
// ...)` passes this same size so the first allocation lands a slot
// big enough for every later user. Single source of truth — pins
// rob's "scan + emit lockstep" invariant. Reset per cgfn.
tagscrsz: i32,
// Live @retscr offset (#14). c.locals-based `@`-prefix dedup in
// localadd is unwound by cgblock save/restore (post-#27), so a
// second `return` in a sibling/outer block reallocates a fresh
// slot — emit grew the frame past what scanlocals reserved, and
// the stomp landed below SP. retscroff is the persistent SSoT:
// 0 means "not yet allocated"; first emit-site sets it, every
// later emit reuses. Mirrors c.tagscrsz pattern (#38) but tracks
// offset, not size (per-fn return type is fixed, so size is too).
retscroff: i32,
// System V AMD64 sret discipline (#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 returns it in RAX.
//
// sretargoff — callee-side @sretarg slot (8B, holds saved RDI).
// Set in cgfn prologue when the fn's return type
// triggers sret. 0 means N/A.
// sretdestoff — caller-side dest BP offset, propagated from a
// receive site (cglet / cgassign ident) to the
// nested cgexpr → cgcall so the call emits
// `LEAQ off(BP), DI` instead of allocating a
// scratch. 0 means no receiver wired.
// sretscroff — per-fn @sretscr discard slot, used by sret CALLs
// whose result has no named receiver. Single-slot
// SSoT mirroring c.retscroff; the scanlocals walk
// sums c.sretscrsz to pre-reserve.
// sretscrsz — max sret discard size in this fn (sums during
// scanlocals, consumed by localadd("@sretscr", ...)).
// sretforward — set by cgreturn `return f();` from an sret callee to
// signal cgcall: source RDI for inner from outer's
// saved @sretarg (MOVQ) instead of LEAQ'ing a local
// dest. Inner writes into outer's caller-prealloc;
// inner's RAX (the dest pointer) is already outer's
// return value. Cleared after cgcall consumes it.
sretargoff: i32,
//
// The single-slot caches for @sretarg / @sretscr / @retscr that
// used to live here are gone: localadd's `@`-prefix dedup against
// c.locals (fail-loud on size grow) is the SSoT now. cgenstmt /
// cgenexpr resolve `@sretarg` via localfind when they need the
// saved RDI.
sretdestoff: i32,
sretscroff: i32,
sretscrsz: i32,
sretforward: i32,
};
@@ -518,16 +515,12 @@ type letvar = struct {
fn cgeninit(c: *cgen, a: *arena) void = {
c.a = a;
c.locals = nil;
c.atlocals = nil;
c.frame = 0;
c.lastwasreturn = 0;
c.labelseq = 0;
c.varargseq = 0;
c.tagscrsz = 0;
c.retscroff = 0;
c.sretargoff = 0;
c.sretdestoff = 0;
c.sretscroff = 0;
c.sretscrsz = 0;
c.sretforward = 0;
// Note: strlit_seq, strlits, ffis are *not* reset here; they
// persist across cgfn calls within one file. cgfile resets them
@@ -542,10 +535,9 @@ fn cgeninit(c: *cgen, a: *arena) void = {
};
// localalloc — append a slot for `name` without dedup. Used for
// match-arm bindings, which C cgen allocates via cgexpr's by-value
// match-arm bindings, which cstage allocates via cgexpr's by-value
// `locals` list — so two separate matches each get fresh slots even
// when their bind names collide. scanlocals follows the same rule
// for nkind.N_MCASE.
// when their bind names collide.
fn localalloc(c: *cgen, name: str, sz: i32, tnode: *node) i32 = {
let asz: i32 = sz;
if (asz < 8) { asz = 8; };
@@ -555,6 +547,7 @@ fn localalloc(c: *cgen, name: str, sz: i32, tnode: *node) i32 = {
let l: *local = amalloc(c.a, 48u64): *local;
l.name = name;
l.off = off;
l.sz = asz;
l.tnode = tnode;
l.lnext = c.locals;
c.locals = l;
@@ -570,6 +563,7 @@ fn localaddstack(c: *cgen, name: str, tnode: *node, off: i32) void = {
let l: *local = amalloc(c.a, 48u64): *local;
l.name = name;
l.off = off;
l.sz = 0;
l.tnode = tnode;
l.lnext = c.locals;
c.locals = l;
@@ -579,86 +573,60 @@ fn localadd(c: *cgen, name: str, sz: i32, tnode: *node) i32 = {
// User-let path (post-#27): always allocate a fresh slot per
// binding. Pre-fix this deduped by name to share one slot
// across same-name lets in disjoint scopes — inherited from
// C cgen's localoff. Both stages had the same silent-stack-
// cstage's localoff. Both stages had the same silent-stack-
// corruption bug: an inner 8B `let a: i64` allocated first
// would force a later outer `let a: [128]u8` onto the 8B slot,
// and `a[127]` would write at +119(BP), past the saved RIP.
// Localfind walks head-first, so the most-recent binding still
// wins lookups inside its scope. Tnode is carried on the
// freshly-pushed entry, so type dispatch in cgenutil never
// sees a stale predecessor.
//
// Synthetic scratch slots (`@tagscr`, `@retscr`, `@tagbase`)
// keep the per-fn dedup. Each scratch is sized identically
// across its call sites and intended to be shared — the
// scanlocals pre-pass also dedups via scanseenmark, so frame
// reservation and emit-time allocation stay in sync. The
// `@`-prefix carve-out preserves that contract; user names
// can never start with `@` (lexer-rejected).
//
// @retscr (#14) routes through c.retscroff instead of c.locals.
// The c.locals-based dedup is unwound by cgblock save/restore
// (post-#27): a return inside an `if` block adds @retscr to
// c.locals; on block exit, c.locals reverts and a sibling/outer
// return reallocates a fresh slot. Scan had reserved one slot;
// emit grew the frame past the reservation and the second
// site's writes landed below SP. c.retscroff is per-fn state
// that survives cgblock save/restore and pins single-slot.
// `@`-prefix scratch slots (`@tagscr`, `@retscr`, `@tagbase`,
// `@sretarg`, `@sretscr`, `@match_spill`, `@vararg_*`) share
// one slot per name per fn. Post #15/#26c the slot is sized
// at first use and reused by every later caller; a later
// caller asking for a larger slot than the first allocation
// pinned fatals (rule 7 — surface, don't silently corrupt
// the frame: the pinned offset already neighbours other
// locals so the slot can't grow in place). Mirrors cstage's
// cg_tagscr / cg_retscr / cg_sretscr same-fn caches in
// cmd/w6c/cgen.c (#26 / #15).
if (name.len > 0) {
if (name[0] == 64u8) { // '@'
if (streq(name, "@retscr")) {
if (c.retscroff != 0) { return c.retscroff; };
let off: i32 = localalloc(c, name, sz, tnode);
c.retscroff = off;
return off;
};
// @sretarg / @sretscr (#23): same single-slot SSoT
// pattern as @retscr. @sretarg holds the saved hidden
// RDI for sret callees (8B, set once per fn at the
// prologue); @sretscr is the caller-side discard slot
// for sret CALLs whose result is dropped.
if (streq(name, "@sretarg")) {
if (c.sretargoff != 0) { return c.sretargoff; };
let off: i32 = localalloc(c, name, sz, tnode);
c.sretargoff = off;
return off;
};
if (streq(name, "@sretscr")) {
if (c.sretscroff != 0) { return c.sretscroff; };
let off: i32 = localalloc(c, name, sz, tnode);
c.sretscroff = off;
return off;
};
let cur: *local = c.locals;
let asz: i32 = sz;
if (asz < 8) { asz = 8; };
if ((asz & 7) != 0) { asz = (asz + 7) & ~7; };
let cur: *local = c.atlocals;
for (cur != nil) {
let cn: str = cur.name;
if (streq(cn, name)) {
if (asz > cur.sz) {
// rule-7 surface, post-#15: pinned slot
// offset can't grow in place.
let msg: str = "localadd: @-prefix slot grew within fn\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
cur.tnode = tnode;
return cur.off;
};
cur = cur.lnext;
};
// First use: allocate via localalloc (bumps c.frame +
// pushes to c.locals so localfind sees it within this
// block) and pin a parallel entry in c.atlocals so the
// allocation survives cgblock save/restore.
let off: i32 = localalloc(c, name, sz, tnode);
let at: *local = amalloc(c.a, 48u64): *local;
at.name = name;
at.off = off;
at.sz = asz;
at.tnode = tnode;
at.lnext = c.atlocals;
c.atlocals = at;
return off;
};
};
return localalloc(c, name, sz, tnode);
};
// scanseenmark — called by scanlocals on every let / match-bind
// site. Returns true if `name` is already tracked in c.locals (so
// the slot will be shared at emission time — no new frame bump).
// Otherwise appends a name-only stub and returns false. Stubs are
// thrown away when cgfn resets c.locals before emission.
fn scanseenmark(c: *cgen, name: str) bool = {
if (localfindnode(c, name) != nil) { return true; };
let l: *local = amalloc(c.a, 48u64): *local;
l.name = name;
l.off = 0;
l.tnode = nil;
l.lnext = c.locals;
c.locals = l;
return false;
};
fn localfindnode(c: *cgen, name: str) *local = {
let l: *local = c.locals;
for (l != nil) {
@@ -666,6 +634,18 @@ fn localfindnode(c: *cgen, name: str) *local = {
if (streq(ln, name)) { return l; };
l = l.lnext;
};
// @-prefix scratch slots survive cgblock save/restore via
// c.atlocals; a localfindnode from a sibling/outer block must
// still resolve them.
if (name.len > 0) {
if (name[0] == 64u8) {
let a: *local = c.atlocals;
for (a != nil) {
if (streq(a.name, name)) { return a; };
a = a.lnext;
};
};
};
return nil;
};
@@ -684,21 +664,92 @@ fn localfind(c: *cgen, name: str) i32 = {
};
l = l.lnext;
};
if (name.len > 0) {
if (name[0] == 64u8) {
let a: *local = c.atlocals;
for (a != nil) {
if (streq(a.name, name)) { return a.off; };
a = a.lnext;
};
};
};
return 0;
};
// ---- emit helpers ---------------------------------------------------
fn emitline(s: str) void = { os.write(1, s.ptr, s.len: u64); };
// Cgfn defers its prologue (TEXT / SUBQ) until after the body so the
// frame size reflects every emit-time localadd — the scanlocals pre-
// pass that previously pre-computed it was dropped per #15/#26c. The
// body is captured into cgoutbuf while cgoutmode != 0, then flushed
// after the prologue is written to stdout. Module-level state so the
// existing emitline/emitint/emitlabel/emitsymname callers don't have
// to thread a *cgen they don't already hold. Mirrors cstage's deferred
// Prog-chain emit (cmd/w6c/cgen.c cgfn allocates `subsp`/`text` up
// front and patches `from.offset` after the body finishes).
let cgoutbuf: *u8 = nil;
let cgoutbufcap: i32 = 0;
let cgoutbuflen: i32 = 0;
let cgoutmode: i32 = 0;
let cgoutarena: *arena = nil;
def CGOUT_INIT_CAP: i32 = 65536;
fn cgout_grow(need: i32) void = {
if (need <= cgoutbufcap) { return; };
let want: i32 = cgoutbufcap;
if (want == 0) { want = CGOUT_INIT_CAP; };
for (want < need) { want = want * 2; };
let p: *u8 = amalloc(cgoutarena, want: u64): *u8;
let i: i32 = 0;
for (i < cgoutbuflen) {
p[i] = cgoutbuf[i];
i += 1;
};
cgoutbuf = p;
cgoutbufcap = want;
};
fn cgout_enable(a: *arena) void = {
cgoutarena = a;
cgoutbuflen = 0;
cgoutmode = 1;
};
fn cgout_disable() void = { cgoutmode = 0; };
fn cgout_flush() void = {
if (cgoutbuflen > 0) {
os.write(1, cgoutbuf, cgoutbuflen: u64);
cgoutbuflen = 0;
};
};
fn emitbytes(p: *u8, n: u64) void = {
if (cgoutmode != 0) {
let nn: i32 = n: i32;
cgout_grow(cgoutbuflen + nn);
let i: i32 = 0;
for (i < nn) {
cgoutbuf[cgoutbuflen + i] = p[i];
i += 1;
};
cgoutbuflen += nn;
} else {
os.write(1, p, n);
};
};
fn emitline(s: str) void = { emitbytes(s.ptr, s.len: u64); };
fn emitint(v: i64) void = {
let s: str = strconv.i64tos(v, strconv.base.DEC);
os.write(1, s.ptr, s.len: u64);
emitbytes(s.ptr, s.len: u64);
};
fn emituint(v: u64) void = {
let s: str = strconv.u64tos(v, strconv.base.DEC);
os.write(1, s.ptr, s.len: u64);
emitbytes(s.ptr, s.len: u64);
};
// emitdispreg — print "disp(reg)" or "(reg)" when disp == 0, the
@@ -755,7 +806,7 @@ fn mklabel(c: *cgen, prefix: str) str = {
};
fn emitlabel(s: str) void = {
os.write(1, s.ptr, s.len: u64);
emitbytes(s.ptr, s.len: u64);
emitline(":\n");
};
@@ -1076,7 +1127,7 @@ fn emitdatawbyte(b: u8) void = {
else { bb[0] = (hi - 10u8) + 97u8; };
if (lo < 10u8) { bb[1] = lo + 48u8; }
else { bb[1] = (lo - 10u8) + 97u8; };
os.write(1, bb.ptr, 2u64);
emitbytes( bb.ptr, 2u64);
return;
};
if (b >= 127u8) {
@@ -1088,12 +1139,12 @@ fn emitdatawbyte(b: u8) void = {
else { bb[0] = (hi - 10u8) + 97u8; };
if (lo < 10u8) { bb[1] = lo + 48u8; }
else { bb[1] = (lo - 10u8) + 97u8; };
os.write(1, bb.ptr, 2u64);
emitbytes( bb.ptr, 2u64);
return;
};
let bb: [1]u8;
bb[0] = b;
os.write(1, bb.ptr, 1u64);
emitbytes( bb.ptr, 1u64);
};
// letpreintern — intern strlits referenced from top-level str-let
@@ -1258,7 +1309,7 @@ fn emitletdataw(c: *cgen, file: *node) void = {
emitline("DATAR ");
emitsymname(c, nm);
emitline("+0(SB),");
os.write(1, lab.ptr, lab.len: u64);
emitbytes( lab.ptr, lab.len: u64);
emitline("(SB)\n");
} else {
// zero-init: accept no rhs, nil,
@@ -1426,12 +1477,12 @@ fn emitdefconstants(c: *cgen, file: *node) void = {
emitline("DATA ");
if (d.exported == 0) {
if (d.nmod.len > 0) {
os.write(1, d.nmod.ptr, d.nmod.len: u64);
os.write(1, ".".ptr, 1u64);
emitbytes( d.nmod.ptr, d.nmod.len: u64);
emitbytes( ".".ptr, 1u64);
};
};
let nm: str = d.str;
os.write(1, nm.ptr, nm.len: u64);
emitbytes( nm.ptr, nm.len: u64);
emitline("(SB),\"");
let i: i32 = 0;
let n: u64 = v;
@@ -1452,7 +1503,7 @@ fn emitdefconstants(c: *cgen, file: *node) void = {
else { bb[0] = (hi - 10u8) + 97u8; };
if (lo < 10u8) { bb[1] = lo + 48u8; }
else { bb[1] = (lo - 10u8) + 97u8; };
os.write(1, bb.ptr, 2u64);
emitbytes( bb.ptr, 2u64);
} else {
if (b >= 127u8) {
emitline("\\x");
@@ -1463,11 +1514,11 @@ fn emitdefconstants(c: *cgen, file: *node) void = {
else { bb[0] = (hi - 10u8) + 97u8; };
if (lo < 10u8) { bb[1] = lo + 48u8; }
else { bb[1] = (lo - 10u8) + 97u8; };
os.write(1, bb.ptr, 2u64);
emitbytes( bb.ptr, 2u64);
} else {
let bb: [1]u8;
bb[0] = b;
os.write(1, bb.ptr, 1u64);
emitbytes( bb.ptr, 1u64);
};
};
};};
@@ -1487,7 +1538,7 @@ fn emitdatasection(c: *cgen) void = {
for (s != nil) {
emitline("DATA ");
let lab: str = s.label;
os.write(1, lab.ptr, lab.len: u64);
emitbytes( lab.ptr, lab.len: u64);
emitline("(SB),\"");
let bs: str = s.bytes;
let i: i32 = 0;
@@ -1508,7 +1559,7 @@ fn emitdatasection(c: *cgen) void = {
else { bb[0] = (hi - 10u8) + 97u8; };
if (lo < 10u8) { bb[1] = lo + 48u8; }
else { bb[1] = (lo - 10u8) + 97u8; };
os.write(1, bb.ptr, 2u64);
emitbytes( bb.ptr, 2u64);
} else {
if (b >= 127u8) {
emitline("\\x");
@@ -1519,11 +1570,11 @@ fn emitdatasection(c: *cgen) void = {
else { bb[0] = (hi - 10u8) + 97u8; };
if (lo < 10u8) { bb[1] = lo + 48u8; }
else { bb[1] = (lo - 10u8) + 97u8; };
os.write(1, bb.ptr, 2u64);
emitbytes( bb.ptr, 2u64);
} else {
let bb: [1]u8;
bb[0] = b;
os.write(1, bb.ptr, 1u64);
emitbytes( bb.ptr, 1u64);
};
};
};};};};};
@@ -1891,15 +1942,15 @@ fn emitsymname(c: *cgen, ident: str) void = {
let resolved: str = ffiresolve(c, ident);
if (resolved.ptr != ident.ptr) {
// FFI hit — emit the mapped linker symbol verbatim.
os.write(1, resolved.ptr, resolved.len: u64);
emitbytes( resolved.ptr, resolved.len: u64);
return;
};
let mod: str = modlookup(c, ident);
if (mod.len > 0) {
os.write(1, mod.ptr, mod.len: u64);
os.write(1, ".".ptr, 1u64);
emitbytes( mod.ptr, mod.len: u64);
emitbytes( ".".ptr, 1u64);
};
os.write(1, ident.ptr, ident.len: u64);
emitbytes( ident.ptr, ident.len: u64);
};
// emitfnname — write the asm symbol name for a fn `ident`, threading
@@ -1910,15 +1961,15 @@ fn emitsymname(c: *cgen, ident: str) void = {
fn emitfnname(c: *cgen, ident: str, hint: str) void = {
let resolved: str = ffiresolve(c, ident);
if (resolved.ptr != ident.ptr) {
os.write(1, resolved.ptr, resolved.len: u64);
emitbytes( resolved.ptr, resolved.len: u64);
return;
};
let mod: str = modlookupforfn(c, ident, hint);
if (mod.len > 0) {
os.write(1, mod.ptr, mod.len: u64);
os.write(1, ".".ptr, 1u64);
emitbytes( mod.ptr, mod.len: u64);
emitbytes( ".".ptr, 1u64);
};
os.write(1, ident.ptr, ident.len: u64);
emitbytes( ident.ptr, ident.len: u64);
};
// ---- FFI map ---------------------------------------------------------

View File

@@ -1,9 +1,10 @@
// selfhost/cmd/wcc/cgendecl.ww — split out of cgen.ww.
//
// Houses the top-level emission glue:
// - scanlocals: frame pre-scan that counts each local `let`
// - cgfnparams: parameter spilling per SysV
// - cgfn: fn prologue + body + epilogue
// - cgfn: fn body emit (TEXT/SUBQ patched after body), prologue
// deferred via cgen.ww's cgoutbuf so the frame size
// reflects every emit-time localadd (#15/#26c)
// - cgfile: file-level entry (the exported driver)
//
// Bundler pulls this in transitively via cgen.ww; consumers don't
@@ -19,519 +20,6 @@ import typ;
import sym;
import strconv;
// tagscrbump — record that the body needs an @tagscr scratch slot of at
// least `need` bytes and return how many additional frame bytes that
// imposes. Each tagged-scratch reservation site calls this; the first
// raises c.tagscrsz from 0, later sites only grow it when they need
// more. Closes STATUS latent #1: pre-fix every site reserved a flat 24B
// and the slot under-allocated for any tagged-union with a 24B+ payload
// (e.g. `(void | err)` where `err` is 24B → slot_sz 32). Cgen-side
// emit (cgreturn / pushargsrev / cgindex / cgwidentaggedstore) reads
// c.tagscrsz to allocate the actual slot — scan + emit see the same
// number, so rob's "lockstep" invariant holds. The @tagscr lifetime is
// short-lived per use (zero, fill, copy out), and uses are sequential
// within a fn body, so sharing the max is safe.
fn tagscrbump(c: *cgen, need: i32) i32 = {
let n: i32 = need;
if (n < 8) { n = 8; };
if ((n & 7) != 0) { n = (n + 7) & ~7; };
if (n <= c.tagscrsz) { return 0; };
let delta: i32 = n - c.tagscrsz;
c.tagscrsz = n;
return delta;
};
// sretscrbump — sister of tagscrbump for the sret discard slot
// (#23). Tracks max sret return type used as a CALL discard / nested
// receiver. Returns frame-byte delta vs the previous high water mark
// (rounded up to 8B).
fn sretscrbump(c: *cgen, need: i32) i32 = {
let n: i32 = need;
if (n < 8) { n = 8; };
if ((n & 7) != 0) { n = (n + 7) & ~7; };
if (n <= c.sretscrsz) { return 0; };
let delta: i32 = n - c.sretscrsz;
c.sretscrsz = n;
return delta;
};
//
// Recursively walks the body to count every local `let`. Each gets a
// slot sized by slotsize(typ); 8-byte default. Match-bindings + for-
// init lets count too. Params are added by the cgfn driver.
fn scanlocals(c: *cgen, n: *node) i32 = {
if (n == nil) { return 0; };
let total: i32 = 0;
if (n.kind == nkind.N_LET) {
// Match localadd's rounding: < 8 bumps to 8, then 8-align.
// scanlocals must agree with localadd or the prologue
// SUBQ undersizes the frame and lets overflow into the
// caller's stack — corrupting whatever's at -frameSize..-1
// of the caller. Post-#27 every let allocates fresh (no
// name dedup), so we always count + always append a stub.
// The stub carries n.lhs as tnode so later scanlocals
// nodes can dispatch on type — e.g. detecting `arr[i] = ...`
// where arr is a tagged-element array (needs @tagscr).
// localfindnode walks head-first, so the freshest stub
// (innermost binding) wins lookup.
let sz: i32 = letslotsize(c, n);
if (sz < 8) { sz = 8; };
if ((sz & 7) != 0) { sz = (sz + 7) & ~7; };
total += sz;
let stub: *local = amalloc(c.a, 48u64): *local;
stub.name = n.str;
stub.off = 0;
stub.tnode = n.lhs;
stub.lnext = c.locals;
c.locals = stub;
};
// Multi-let from a tuple-returning call: each binding's size
// comes from its annotated type (l.lhs) when present, else from
// the rhs call's return-tuple element type. Marking via
// scanseenmark also dedupes the recursive descent into n.list
// so each child isn't counted again at the default 8B.
if (n.kind == nkind.N_MLET) {
let p0t: *node = nil;
let p1t: *node = nil;
if (n.rhs != nil) {
if (n.rhs.kind == nkind.N_CALL) {
let callee: *node = n.rhs.lhs;
if (callee != nil) {
let cnm: str;
cnm.ptr = nil; cnm.len = 0;
if (callee.kind == nkind.N_IDENT) { cnm = callee.str; };
if (callee.kind == nkind.N_DOT) { cnm = callee.str; };
if (cnm.len > 0) {
let rt: *node = fnretlookup(c, cnm);
if (rt != nil) {
if (rt.kind == nkind.N_TTUPLE) {
p0t = rt.list;
if (p0t != nil) { p1t = p0t.next; };
};
};
};
};
};
};
let l: *node = n.list;
let pt: *node = p0t;
let bidx: i32 = 0;
for (l != nil) {
let t: *node = l.lhs;
if (t == nil) {
if (bidx == 0) { t = p0t; };
if (bidx == 1) { t = p1t; };
};
let sz: i32 = 8;
if (t != nil) { sz = slotsize(c, t); };
if (sz < 8) { sz = 8; };
if ((sz & 7) != 0) { sz = (sz + 7) & ~7; };
total += sz;
// Always-fresh stub (post-#27); tnode carries the
// binding's type so later array-index dispatch can
// resolve the let through localfindnode.
let stub: *local = amalloc(c.a, 48u64): *local;
stub.name = l.str;
stub.off = 0;
stub.tnode = t;
stub.lnext = c.locals;
c.locals = stub;
l = l.next;
bidx += 1;
};
};
// `switch` allocates an 8B scratch slot for the scrutinee so case
// bodies can spill through SP without losing it. The slot is named
// ".sw_<labelseq>" at cgen time — unique per switch — so it must
// not dedup. Count it here so the frame SUBQ matches.
if (n.kind == nkind.N_SWITCH) { total += 8; };
// `for (let x .. s)` allocates two 8B scratch slots — `.rgi_<seq>`
// (counter) and `.rgl_<seq>` (length) — plus one slot per binding.
// Per-binding sz defaults to 8 (covers scalar primitives + ptrs).
// `str` tuple-fields would need 16 — selfhost doesn't yet emit
// those, so the simple count tracks C cgen for current fixtures.
if (n.kind == nkind.N_FORRANGE) {
total += 16; // .rgi + .rgl scratch
// Each forrange binding gets a fresh 8B slot (post-#27).
// Stub is also appended so the body's references resolve
// to this binding via head-first localfindnode lookup.
if (n.list != nil) {
let m: *node = n.list;
for (m != nil) {
let bnm: str = m.str;
total += 8;
if (bnm.len > 0) {
let stub: *local = amalloc(c.a, 48u64): *local;
stub.name = bnm;
stub.off = 0;
stub.tnode = m.lhs;
stub.lnext = c.locals;
c.locals = stub;
};
m = m.next;
};
} else {
let bnm: str = n.str;
total += 8;
if (bnm.len > 0) {
let stub: *local = amalloc(c.a, 48u64): *local;
stub.name = bnm;
stub.off = 0;
stub.tnode = nil;
stub.lnext = c.locals;
c.locals = stub;
};
};
};
// `match (non-ident)` needs an `@match_spill` scratch slot sized
// to the scrutinee's tagged-union slot (16/24/32 for 1/2/3-word
// payload). Mirrors cstage's `slot_size = su->size` default 16
// in cmd/w6c/cgen.c cgmatch (task #9 align-down to cstage).
// matchspillsz must agree with cgmatch's emit-time computation
// for scan+emit lockstep. N_IDENT scrutinees read the slot
// directly off the local — no spill needed.
if (n.kind == nkind.N_MATCH) {
let sc: *node = n.lhs;
if (sc != nil) {
if (sc.kind != nkind.N_IDENT) {
total += matchspillsz(c, matchscrutt(c, sc));
};
};
};
// Match-arm binding (`case let v: T => ...`) gets a slot too.
// Crucially we do NOT dedup these against c.locals: C cgen
// handles a match as an expression with a by-value locals copy,
// so two separate matches in the same function each allocate
// their `v`/`e` slots fresh. Treating these as deduped would
// shrink the frame below what localadd then bumps it to.
if (n.kind == nkind.N_MCASE) {
let bn: str = n.str;
if (bn.len > 0) {
let pat: *node = n.lhs;
if (pat != nil) {
// Must mirror cgmatch's bind-slot sizing in
// cgenexpr.ww (`bsz = slotsize(c, pat)`):
// hardcoding str/slice/8 here underbooked the
// frame for TY_STRUCT variants — the emit-time
// localalloc(bsz=24) then wrote past the SUBQ'd
// SP, smashing whatever the OS put under it
// (project #31).
let psz: i32 = slotsize(c, pat);
if (psz <= 0) { psz = 8; };
if ((psz & 7) != 0) { psz = (psz + 7) & ~7; };
total += psz;
};
};
// Match arms get a fresh local scope at emission time
// (cgmatch saves c.locals before each arm and restores
// after). scanlocals must mirror that: walk the arm
// body with a saved/restored seenmark set so two arms
// declaring the same name each get their own slot,
// matching the per-arm frame growth the emit phase
// produces.
if (n.body != nil) {
let saved: *local = c.locals;
total += scanlocals(c, n.body);
c.locals = saved;
};
return total;
};
// Tagged-arr/slice index store needs an @tagscr scratch slot for
// cgwidentaggedstore to materialise the source in before copying
// to the element address. Slot is shared per function via
// c.tagscrsz (raised to the largest element slot_sz seen).
// N_DOT-base graduated (task #30) so `obj.arr[i] = v` for an
// [N]Tagged struct field pre-reserves the slot — parallels the
// cgassign N_DOT-base elemtn fix in cgenexpr.ww. Without this
// arm the runtime localadd allocates past the frame boundary
// and clobbers live locals; cstage handles the shape naturally
// via first-use+fail-loud (rule-10 convergence filed as #15).
if (n.kind == nkind.N_ASSIGN) {
let alhs: *node = n.lhs;
if (alhs != nil) {
if (alhs.kind == nkind.N_INDEX) {
let abase: *node = alhs.lhs;
if (abase != nil) {
if (abase.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, abase.str);
let btn: *node = nil;
if (lc != nil) { btn = lc.tnode; }
else { btn = letvartnode(c, abase.str); };
if (btn != nil) {
let bk: nkind = btn.kind;
let etn: *node = nil;
if (bk == nkind.N_TARRAY) { etn = btn.lhs; };
if (bk == nkind.N_TSLICE) { etn = btn.lhs; };
if (bk == nkind.N_TPTR) { etn = btn.lhs; };
if (etn != nil) {
if (istaggedtype(c, etn)) {
total += tagscrbump(c, slotsize(c, etn));
};
};
};
};
if (abase.kind == nkind.N_DOT) {
let ft: *node = dotfieldtnode(c, abase);
if (ft != nil) {
let bk: nkind = ft.kind;
let etn: *node = nil;
if (bk == nkind.N_TARRAY) { etn = ft.lhs; };
if (bk == nkind.N_TSLICE) { etn = ft.lhs; };
if (bk == nkind.N_TPTR) { etn = ft.lhs; };
if (etn != nil) {
if (istaggedtype(c, etn)) {
total += tagscrbump(c, slotsize(c, etn));
};
};
};
};
};
};
};
};
// Tagged-union struct-field write: `s.f = v` or `(*p).f = v`
// where f is a tagged-union field. cgassign delegates to
// cgwidentaggedstore; for pointer-rooted dst the wrapper
// allocates @tagbase (8B, fixed) and @tagscr (sized to the
// field's tagged slot). The @tagscr size feeds c.tagscrsz.
if (n.kind == nkind.N_ASSIGN) {
let alhs: *node = n.lhs;
if (alhs != nil) {
if (alhs.kind == nkind.N_DOT) {
let abase: *node = alhs.lhs;
if (abase != nil) {
if (abase.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, abase.str);
let btn: *node = nil;
if (lc != nil) { btn = lc.tnode; }
else { btn = letvartnode(c, abase.str); };
let isptr: bool = false;
let stype: *node = nil;
if (btn != nil) {
if (btn.kind == nkind.N_TPTR) {
isptr = true;
stype = btn.lhs;
};
if (btn.kind == nkind.N_TNAME) { stype = btn; };
};
if (stype != nil) {
if (stype.kind == nkind.N_TNAME) {
let si: *structinfo = structlookup(c, stype.str);
if (si != nil) {
let fi: *fieldinfo = si.fields;
for (fi != nil) {
if (streq(fi.fname, alhs.str)) {
if (istaggedtype(c, fi.tnode)) {
if (isptr) {
if (!scanseenmark(c, "@tagbase")) {
total += 8;
};
total += tagscrbump(c, slotsize(c, fi.tnode));
};
};
fi = nil;
} else {
fi = fi.finext;
};
};
};
};
};
};
};
};
};
};
// Tagged-union return with struct payload or tagged-subset
// source — cgreturn materialises in @tagscr then loads AX/DX/
// CX/R8. Detect via the same rhsstructpayload predicate the
// cgen uses, so we only reserve when the cgen will actually
// emit a scratch-using path. `!void` / `!i32` aliases share
// N_STRUCTLIT shape but resolve to non-struct types — they
// fall through to scalar/str and don't need scratch. Slot is
// sized to the return type's slot_sz (was hardcoded 24, which
// truncated 32B slots — `(void | err24)` clobbered its own
// payload local; task #38).
if (n.kind == nkind.N_RETURN) {
if (c.fnret != nil) {
if (istaggedtype(c, c.fnret)) {
if (!isnullabletype(c.fnret)) {
let rhs: *node = n.lhs;
let needs: bool = false;
if (rhs != nil) {
let sn: str = rhsstructpayload(c, rhs);
if (sn.len > 0) { needs = true; };
if (rhs.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, rhs.str);
if (lc != nil) {
if (istaggedtype(c, lc.tnode)) {
needs = true;
};
};
};
};
if (needs) {
total += tagscrbump(c, slotsize(c, c.fnret));
};
};
};
};
};
// Whole-struct return for sizes <= 24B uses @retscr — when
// the function's return type is a registered TY_STRUCT of
// size <= 24 and the return rhs is N_IDENT or N_STRUCTLIT,
// cgreturn materialises in @retscr then loads AX/DX/CX.
// Mirrors cstage cgen.c which allocates the scratch slot
// inline; here we must pre-reserve so the prologue SUBQ
// reserves enough frame.
if (n.kind == nkind.N_RETURN) {
if (c.fnret != nil) {
if (c.fnret.kind == nkind.N_TNAME) {
let rname: str = c.fnret.str;
let rsi: *structinfo = structlookup(c, rname);
if (rsi != nil) {
if (rsi.totsize <= 24) {
let rhs: *node = n.lhs;
let okrhs: bool = false;
if (rhs != nil) {
if (rhs.kind == nkind.N_IDENT) {
okrhs = true;
};
if (rhs.kind == nkind.N_STRUCTLIT) {
okrhs = true;
};
};
if (okrhs) {
if (!scanseenmark(c, "@retscr")) {
total += 24;
};
};
};
};
};
};
};
// sret CALL (#23): callee returns plain TY_STRUCT > 24B. The
// receive site (cglet / cgassign ident) overrides at emit time
// with the dest local's own slot; discards / nested calls fall
// back to @sretscr. Single-slot per fn sized to the max sret
// return — sretscrbump tracks the high-water mark so a later
// larger call grows the frame without re-counting the prior
// reservation. Mirrors @tagscr's cumulative tagscrbump.
if (n.kind == nkind.N_CALL) {
let scs: i32 = callsretsize(c, n);
if (scs > 0) { total += sretscrbump(c, scs); };
};
// Call-site struct-payload widening uses @tagscr — when the
// arg is a struct literal/ident and the callee's param is
// tagged, pushargsrev materialises in scratch and pushes.
// Scalar / str args take the direct-push fast path (no
// scratch). Tagged-typed ident args also skip widening (the
// slot is already laid out, so pushargsrev pushes slot words
// directly). Both fast paths agree with C cgen bytewise, so
// only struct-payload sites get a scratch reservation.
if (n.kind == nkind.N_CALL) {
let callee: *node = n.lhs;
let cnm: str;
cnm.ptr = nil; cnm.len = 0;
if (callee != nil) {
if (callee.kind == nkind.N_IDENT) { cnm = callee.str; };
if (callee.kind == nkind.N_DOT) { cnm = callee.str; };
};
if (cnm.len > 0) {
let ps: *node = fnparamslookup(c, cnm);
let a: *node = n.list;
for (a != nil) {
if (ps == nil) { a = nil; }
else {
if (ps.kind == nkind.N_PARAM) {
let pt: *node = ps.lhs;
if (istaggedtype(c, pt)) {
if (!isnullabletype(pt)) {
let sn: str = rhsstructpayload(c, a);
if (sn.len > 0) {
let isidentstruct: bool = false;
if (a.kind == nkind.N_IDENT) {
// Struct ident as
// tagged arg — pushargsrev
// still routes through the
// scratch path.
isidentstruct = true;
};
let _u: bool = isidentstruct;
total += tagscrbump(c, slotsize(c, pt));
};
};
};
};
if (a != nil) {
a = a.next;
ps = ps.next;
};
};
};
};
// Hare-style variadic call: reserve @vararg_d_<seq> for the
// element data and @vararg_sl_<seq> for the 24B slice
// descriptor. The seq is recorded on the N_CALL node so
// cgcall picks the same names regardless of walk order
// (scanlocals descends LTR; pushargsrev evaluates RTL).
let nfixed: i32 = 0;
let varp: *node = callee_variadic_param(c, n.lhs, &nfixed);
if (varp != nil) {
let nargs: i32 = 0;
let aw: *node = n.list;
for (aw != nil) { nargs += 1; aw = aw.next; };
let nvar: i32 = nargs - nfixed;
if (nvar < 0) { nvar = 0; };
let forwarding: bool = false;
if (nvar == 1) {
let aa: *node = n.list;
let k0: i32 = 0;
for (k0 < nfixed) { aa = aa.next; k0 += 1; };
if (aa != nil) {
if (aa.kind == nkind.N_SPREAD) {
forwarding = true;
};
};
};
if (!forwarding) {
let seq: i32 = c.varargseq;
n.uval = seq: u64;
c.varargseq += 1;
let esz: i32 = slotsize(c, varp.lhs);
if (esz < 1) { esz = 1; };
let dname: str = mkvarargname(c, "@vararg_d_", seq);
let sname: str = mkvarargname(c, "@vararg_sl_", seq);
if (nvar > 0) {
if (!scanseenmark(c, dname)) {
let dsz: i32 = nvar * esz;
if ((dsz & 7) != 0) {
dsz = (dsz + 7) & ~7;
};
total += dsz;
};
};
if (!scanseenmark(c, sname)) { total += 24; };
};
};
};
if (n.lhs != nil) { total += scanlocals(c, n.lhs); };
if (n.rhs != nil) { total += scanlocals(c, n.rhs); };
if (n.cond != nil) { total += scanlocals(c, n.cond); };
if (n.body != nil) { total += scanlocals(c, n.body); };
if (n.els != nil) { total += scanlocals(c, n.els); };
if (n.list != nil) {
let m: *node = n.list;
for (m != nil) {
total += scanlocals(c, m);
m = m.next;
};
};
return total;
};
// ---- function-level cgen ---------------------------------------------
@@ -541,7 +29,7 @@ fn cgfnparams(c: *cgen, params: *node) void = {
// (already spilled to @sretarg by cgfn); the first user param
// lands in SI.
let idx: i32 = 0;
if (c.sretargoff != 0) { idx = 1; };
if (localfind(c, "@sretarg") != 0) { idx = 1; };
let fidx: i32 = 0;
// Cursor for args that overflow the SysV reg windows. Each
// stack-passed arg lives at 16+8*k(BP) — no spill, the local
@@ -854,130 +342,18 @@ fn cgfn(c: *cgen, fn_: *node) void = {
// sret callee (#23): return type is plain TY_STRUCT > 24B.
// Reserve 8B for @sretarg (holds the saved hidden RDI dest
// pointer); cgfnparams skips DI for user args, cgreturn writes
// through *(@sretarg) and returns @sretarg in RAX. Decision
// made here so the frame pre-scan and cgfnparams see the same
// view of the int-arg cursor.
// through *(@sretarg) and returns @sretarg in RAX.
let sret_callee: bool = sretretsize(c, c.fnret) > 0;
// Emit the TEXT label via emitfnname so the def site picks up the
// same skip rule (FFI / `main` / empty-module) and the same module
// hint (this fn's own module) that the call sites use. Drops the
// `exported == 0` skip in the legacy inline form — exported fns
// now mangle too, so cross-module same-leaf exports coexist.
emitline("TEXT ");
emitfnname(c, fn_.str, fn_.nmod);
emitline(",$");
// Pre-scan total frame: only count params that land in a local
// slot. SysV-class accounting; mirrors runtime walk in cstage
// cgen.c §5130-5223 and cgfnparams below. A stack-spilled param
// is addressed at a positive BP offset by cgfnparams (via
// localaddstack) and consumes no frame, so adding its size here
// would over-allocate. Seed c.locals with param-name stubs so
// scanlocals dedups a re-declared `let <name>` in the body
// against the param's slot (matches C cgen). Stubs get cleared
// before emission.
let scanp: *node = fn_.list;
let frame: i32 = 0;
let argi: i32 = 0;
let fargi: i32 = 0;
// Reserve @sretarg (8B) BEFORE the param-induced frame, and
// start argi at 1 so the param walker sees RDI as consumed.
if (sret_callee) {
frame += 8;
argi = 1;
};
for (scanp != nil) {
if (scanp.kind == nkind.N_PARAM) {
let isvar: bool = scanp.op == tkind.TK_ELLIPSIS;
let isf: bool = false;
let istg: bool = false;
let issl: bool = false;
let isst: bool = false;
let isstruct: bool = false;
let structsz: i32 = 0;
if (!isvar) {
isf = isfloattype(c, scanp.lhs);
istg = istaggedtype(c, scanp.lhs);
if (!isf && !istg) {
issl = isslicetype(c, scanp.lhs);
if (!issl) { isst = isstrtype(c, scanp.lhs); };
if (!issl && !isst) {
structsz = structparamsize(c, scanp.lhs);
isstruct = structsz > 0;
};
};
};
let eb: i32 = 1;
let sz: i32 = 8;
if (isvar) { eb = 3; sz = 24; }
else { if (istg) { sz = slotsize(c, scanp.lhs); eb = sz / 8; }
else { if (issl) { eb = 3; sz = 24; }
else { if (isst) { eb = 2; sz = 16; }
else { if (isstruct) {
sz = structsz;
eb = 1;
if (structsz > 8) { eb = 2; };
}
else { if (isf) {
eb = 1;
sz = 8;
if (isf32type(c, scanp.lhs)) { sz = 4; };
}; }; }; }; }; };
let regs_left: i32 = 6 - argi;
if (isf) { regs_left = 8 - fargi; };
if (regs_left >= eb) {
frame += sz;
if (isf) { fargi += 1; }
else { argi += eb; };
} else { if (eb > 1 && regs_left > 0 && (istg || issl || isst || isstruct || isvar)) {
// Multi-word param straddles the reg/stack boundary;
// cgfnparams stitches the tail from positive BP
// offsets into a single local slot, so we still
// reserve the full size. Symmetric across tagged,
// slice, str and variadic `T...`
// (cgendecl.ww:467/518/564/394).
frame += sz;
argi = 6;
} else {
// Pure stack: lives at +BP(16+stkcursor*8); no
// local slot consumed. The reg cursor stays put.
}; };
// Record the param's tnode on the stub so scanlocals's
// `h.f = v` / `&h[i]` / etc. detection paths can
// resolve a *struct / *[]T / *T param through
// localfindnode rather than seeing tnode=nil and
// skipping the reservation. Latent pre-#38: the
// pointer-rooted struct-field tagged write
// (cgendecl.ww:264) never fired for `fn fill(h: *holder)
// { h.e = v; }` because the param stub had no type
// info, so @tagscr / @tagbase weren't counted in the
// frame. Emit-time localadd happened to fit pre-#38
// because the 24B hardcoded slot didn't collide with
// the 8B @tagbase neighbour, but a correctly-sized
// slot revealed the under-reservation.
if (!scanseenmark(c, scanp.str)) {
c.locals.tnode = scanp.lhs;
};
};
scanp = scanp.next;
};
c.varargseq = 0;
if (fn_.body != nil) { frame += scanlocals(c, fn_.body); };
c.varargseq = 0;
// Drop the stubs so emission rebuilds c.locals with real offsets.
c.locals = nil;
if ((frame & 15) != 0) {
frame = (frame + 15) & ~15;
};
emitint(frame: i64);
emitline("\n");
emitline("\tPUSHQ\tBP\n");
emitline("\tMOVQ\tSP, BP\n");
emitline("\tSUBQ\t$");
emitint(frame: i64);
emitline(", SP\n");
// Capture the body into cgoutbuf while c.frame grows under
// emit-time localadd calls (#15/#26c — wwstage dropped its
// scanlocals pre-pass to align DOWN with cstage's first-use
// pattern). The prologue (TEXT label, PUSHQ/MOVQ/SUBQ) emits
// after the body finishes so the frame size reflects every
// localadd. Mirrors cstage cmd/w6c/cgen.c cgfn which builds
// `subsp`/`text` Progs up front and patches their `from.offset`
// at the end via txt_emit.
cgout_enable(c.a);
if (sret_callee) {
let saoff: i32 = localadd(c, "@sretarg", 8, nil);
@@ -1011,7 +387,7 @@ fn cgfn(c: *cgen, fn_: *node) void = {
// Run any registered defers in LIFO order before the
// implicit return.
rundefers(c);
// Zero AX before the fall-through return — matches C cgen,
// Zero AX before the fall-through return — matches cstage,
// which always emits this so void-returning fns don't leak
// a stale callee value to their caller.
emitline("\tMOVQ\t$0, AX\n");
@@ -1019,6 +395,28 @@ fn cgfn(c: *cgen, fn_: *node) void = {
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
};
cgout_disable();
let frame: i32 = c.frame;
if ((frame & 15) != 0) { frame = (frame + 15) & ~15; };
// Emit the TEXT label via emitfnname so the def site picks up the
// same skip rule (FFI / `main` / empty-module) and the same module
// hint (this fn's own module) that the call sites use.
emitline("TEXT ");
emitfnname(c, fn_.str, fn_.nmod);
emitline(",$");
emitint(frame: i64);
emitline("\n");
emitline("\tPUSHQ\tBP\n");
emitline("\tMOVQ\tSP, BP\n");
emitline("\tSUBQ\t$");
emitint(frame: i64);
emitline(", SP\n");
cgout_flush();
};
// ---- file-level entry ------------------------------------------------

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;

View File

@@ -193,13 +193,19 @@ fn cgreturn(c: *cgen, n: *node) void = {
};
if (needswiden) {
let rsz: i32 = slotsize(c, c.fnret);
// Use c.tagscrsz so the first @tagscr allocation in
// the fn lands a slot sized to the *max* across all
// uses (scanlocals bumped to rsz here). Hardcoding 24
// truncated 32B-slot returns and overwrote adjacent
// locals during the pre-zero loop (#38).
let scroff: i32 = localadd(c, "@tagscr",
c.tagscrsz, nil);
// @retscr (not @tagscr) for the return materialise
// path. Cstage cmd/w6c/cgen.c cgreturn uses
// `@retscr` here and reserves the @tagscr SSoT
// for arg-widen / non-BP-base store / N_INDEX
// tagged-element write. Sharing the name in a fn
// that BOTH returns a 32B tagged AND pushes a
// smaller tagged arg fatals localadd's @-prefix
// size-grow guard (rule 7); routing returns
// through their own slot keeps each cache
// monotonic. Hardcoding 24 truncated 32B-slot
// returns and overwrote adjacent locals during
// the pre-zero loop (#38).
let scroff: i32 = localadd(c, "@retscr", rsz, nil);
emitline("\tXORQ\tAX, AX\n");
let zz: i32 = 0;
for (zz < rsz) {
@@ -299,7 +305,8 @@ fn cgreturn(c: *cgen, n: *node) void = {
// the SysV "return the pointer" discipline. Two rhs shapes
// are wired: N_IDENT (word-copy from rhs slot to *(dest))
// and N_STRUCTLIT (cgstructlitfill with mode=1 PTR_LOCAL).
if (c.sretargoff != 0) {
let sretargoff: i32 = localfind(c, "@sretarg");
if (sretargoff != 0) {
let scs: i32 = sretretsize(c, c.fnret);
if (scs > 0) {
// sret return-forwarding (task #9 follow-up to
@@ -317,7 +324,7 @@ fn cgreturn(c: *cgen, n: *node) void = {
c.sretforward = 1;
cgexpr(c, rhs);
emitline("\tMOVQ\t");
emitoff(c.sretargoff: i64);
emitoff(sretargoff: i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
@@ -346,14 +353,14 @@ fn cgreturn(c: *cgen, n: *node) void = {
// each field store. disp = 0 because
// the dest pointer IS the struct base.
cgstructlitfill(c, sret_si, rhs,
1, c.sretargoff, emptys,
1, sretargoff, emptys,
0, scs);
};
} else {
let rl: *local = localfindnode(c, rhs.str);
if (rl != nil) {
emitline("\tMOVQ\t");
emitoff(c.sretargoff: i64);
emitoff(sretargoff: i64);
emitline("(BP), BX\n");
let k: i32 = 0;
for (k + 8 <= scs) {
@@ -387,7 +394,7 @@ fn cgreturn(c: *cgen, n: *node) void = {
};
// sret return: RAX = dest pointer.
emitline("\tMOVQ\t");
emitoff(c.sretargoff: i64);
emitoff(sretargoff: i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");

View File

@@ -71,9 +71,9 @@ fn callee_variadic_param(c: *cgen, callee: *node, nfixed_out: *i32) *node = {
// mkvarargname — fresh local-slot name "<prefix><seq>". Used for
// the per-variadic-call scratch buffers (`@vararg_d_N` for the
// element-data buffer, `@vararg_sl_N` for the 24B slice descriptor)
// where N is recorded on the N_CALL node at scanlocals time so both
// the prologue reservation and the call-site emission agree.
// element-data buffer, `@vararg_sl_N` for the 24B slice descriptor).
// N is recorded on the N_CALL node at first emit so re-entry into
// cgcall picks the same names regardless of walk order.
fn mkvarargname(c: *cgen, prefix: str, seq: i32) str = {
let buf: [128]u8;
let i: i32 = 0;
@@ -179,7 +179,7 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = {
let pname: str = rhsstructpayload(c, arg);
if (pname.len > 0) {
let ptype: *node = param.lhs;
let scroff: i32 = localadd(c, "@tagscr", c.tagscrsz, nil);
let scroff: i32 = localadd(c, "@tagscr", widensz, nil);
emitline("\tXORQ\tAX, AX\n");
let zz: i32 = 0;
for (zz < widensz) {
@@ -1747,8 +1747,8 @@ fn inferletcalltype(c: *cgen, rhs: *node) *node = {
// letslotsize — slot size for a `let` binding. Like slotsize, but
// detects `[_]T = arrlit;` (the type-AST has rhs == nil as the
// length-inferred sentinel) and computes count × element-size from
// the initialiser. Used by both scanlocals (prologue sizing) and
// cglet (slot alloc) so they agree on the frame layout.
// the initialiser. Called from cglet at emit time so the frame
// grows monotonically per first-use (#15).
//
// `let x = f();` (no annotation): infer from `f`'s declared return
// type so a 24B tagged-union return reserves all three spill slots,
@@ -2119,9 +2119,8 @@ export fn resolvetagged(c: *cgen, t: *node) *node = {
};
// matchscrutt — resolve a non-ident match scrutinee node to its tagged
// type (or nil if unresolvable). Mirrors cgmatch's inline scrutinee
// type resolution; factored so cgmatch (emit) and scanlocals (count)
// agree on the spill slot's size per the scan+emit lockstep invariant.
// type (or nil if unresolvable). Used by cgmatch to size the
// @match_spill slot at first use (#15 first-use+fail-loud convergence).
// IDENT scrutinees use a different lookup path (read off the local
// directly, no spill) so this returns nil for them too.
fn matchscrutt(c: *cgen, scrut: *node) *node = {
@@ -2184,8 +2183,8 @@ fn matchscrutt(c: *cgen, scrut: *node) *node = {
// scrutinee lands in. Mirrors cstage's `slot_size = (su->kind ==
// TY_TAGGED) ? su->size : 16` (cmd/w6c/cgen.c cgmatch). 16 default
// when the scrutinee type can't be resolved keeps the historical
// alloc for non-tagged / unresolved cases. Used by both scanlocals
// (counting) and cgmatch (emitting) per rule-10 align-to-cstage.
// alloc for non-tagged / unresolved cases. Called by cgmatch at first
// use; #15 first-use+fail-loud pins this size per fn.
fn matchspillsz(c: *cgen, scrutt: *node) i32 = {
if (scrutt == nil) { return 16; };
let sz: i32 = slotsize(c, scrutt);
@@ -2972,11 +2971,11 @@ fn cgwidentaggedstore(c: *cgen, dst: *node, src: *node,
emitline(", ");
emitoff(bspill: i64);
emitline("(BP)\n");
// Same shared scratch — c.tagscrsz is the per-fn max across every
// reservation site (scanlocals); pinning to slot_sz here would
// undersize the slot if a sibling site (cgreturn, pushargsrev,
// cgindex) needed a larger one and fired second.
let scr: i32 = localadd(c, "@tagscr", c.tagscrsz, nil);
// Shared scratch sized at first use per #15/#26c. A sibling
// site (cgreturn, pushargsrev, cgindex) hitting @tagscr later
// with a larger size fatals (rule 7) — pinned offset can't
// grow in place.
let scr: i32 = localadd(c, "@tagscr", slot_sz, nil);
emitline("\tXORQ\tAX, AX\n");
let z: i32 = 0;
for (z < slot_sz) {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,293 @@
/*
* 748_size_strategy_convergence — sentinel for #15/#26c (subsumes #36).
*
* Pins wwstage's first-use+fail-loud frame-sizing convergence with
* cstage. Pre-#15 wwstage ran a `scanlocals` pre-pass that walked the
* body to pre-size the SUBQ slot total; cstage allocated slots at
* emit time and patched the prologue after the body finished. The
* pre-pass under-counted match-arm `case let` bindings in `(str|rune)
* ...` callees (#36 original surface): variadic param tnode was the
* inner type, not the synthesised slice wrap, so the @match_spill
* lookup at scan time fell back to the default (16) instead of the
* real slot (24). Emit-time cgmatch then resolved through the slice-
* wrapped local and chose 24, growing c.frame past the SUBQ reservation;
* `case let r: rune =>` writes landed below SP.
*
* Post-fix both stages defer the prologue until after the body emits,
* and every @-prefix scratch slot (@tagscr / @retscr / @sretscr /
* @tagbase / @sretarg / @vararg_*) is sized at first use. A later
* caller asking for a larger slot than the first allocation pinned
* fatals (rule 7 — pinned offset can't grow in place; #26 already
* proved the model for cstage's @tagscr cache). The variadic gather
* also routes the element stride through the raw type size (matching
* cstage's velem->size) so the callee read at `arg[i]` lines up with
* the caller's store.
*
* Rows assert per-stage runtime and cstage-vs-wwstage byte-id on the
* shapes that #36 + #15 are pinned against.
*
* row | what it pins
* -------------------------------+--------------------------------
* tag_variadic_runearm | #36 original surface — rune-arm
* | of `(str|rune)...` callee. Pre-
* | fix wwstage SUBQ $80, cstage $96.
* | Post-fix byte-id on the callee.
* trim_iter_match_prev | #36 sibling — `trim: rune...` +
* | strings.iter outer + nested
* | strings.prev match. Pre-fix both
* | stages framed $192 but match-arm
* | scratch offsets diverged inside
* | the same frame. Post-fix loop_for
* | byte-id; runtime returns 0.
* variadic_gather_rune_stride | Caller-side gather stride for
* | `(rune...)` must use raw u32
* | size (4), not slotsize (8 —
* | stack-padded). Pre-fix wwstage
* | MOVQ @ 8B stride vs cstage MOVL
* | @ 4B; callee's `arg[i]` read at
* | 4B stride saw garbage at odd
* | indices.
* leaf_baseline | Leaf fn match: no nested blocks,
* | no scratch slot cache interaction.
* | Confirms the deferred-prologue
* | refactor didn't regress the
* | simple frame size.
*/
#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;
int want;
};
static const struct row rows[] = {
/* 1. #36 original surface: rune-arm of `(str|rune)...` callee.
* want("hello", 'X', "world") → 5 + 1 + 5 = 11. */
{ "tag_variadic_runearm",
"package main;\n"
"import os;\n"
"fn want(args: (str | rune)...) i64 = {\n"
" let total: i64 = 0;\n"
" let i: i32 = 0;\n"
" for (i < args.len) {\n"
" match (args[i]) {\n"
" case let s: str => total += s.len: i64;\n"
" case let r: rune => total += 1;\n"
" };\n"
" i += 1;\n"
" };\n"
" return total;\n"
"};\n"
"export fn main() i32 = {\n"
" let n: i64 = want(\"hello\", 'X', \"world\");\n"
" return n: i32;\n"
"};\n",
11 },
/* 2. #36 sibling: variadic + iter + nested match prev in non-leaf.
* Mirrors .ai/probe_trim_36extra.ww. loop_for("aabcc", 'a', 'b')
* trims both leading 'a's and the leading 'b' → "cc" (len 2). */
{ "trim_iter_match_prev",
"package main;\n"
"import strings;\n"
"import encoding.utf8;\n"
"fn loop_for(input: str, trim: rune...) str = {\n"
" let it: strings.iterator = strings.iter(input);\n"
" for (true) {\n"
" match (strings.next(&it)) {\n"
" case let r: rune => {\n"
" let j: i32 = 0;\n"
" let found: bool = false;\n"
" for (j < trim.len) {\n"
" if (r == trim[j]) { found = true; j = trim.len; }\n"
" else { j += 1; };\n"
" };\n"
" if (!found) {\n"
" match (strings.prev(&it)) {\n"
" case let r2: rune => void;\n"
" case utf8.done => void;\n"
" };\n"
" break;\n"
" };\n"
" };\n"
" case utf8.done => break;\n"
" };\n"
" };\n"
" return strings.iterstr(&it);\n"
"};\n"
"export fn main() i32 = {\n"
" let r: str = loop_for(\"aabcc\", 'a', 'b');\n"
" if (r.len != 2) { return 11; };\n"
" return 0;\n"
"};\n",
0 },
/* 3. Caller-side variadic gather stride. The callee reads `arg[j]`
* at the raw element size (4 for rune); the gather must match.
* Pre-fix wwstage stored at slotsize (8B stride) and the callee
* read garbage for j=1 — the test would have returned `'b'` (98)
* or 0 depending on the high half of the stack word. Post-fix the
* caller gathers at 4B stride and `arg[1]` reads 'b' correctly. */
{ "variadic_gather_rune_stride",
"package main;\n"
"fn pick(idx: i32, args: rune...) i32 = {\n"
" if (idx >= args.len) { return 0; };\n"
" return args[idx]: i32;\n"
"};\n"
"export fn main() i32 = {\n"
" let r0: i32 = pick(0, 'a', 'b', 'c');\n"
" let r1: i32 = pick(1, 'a', 'b', 'c');\n"
" let r2: i32 = pick(2, 'a', 'b', 'c');\n"
" if (r0 != 97) { return 11; };\n"
" if (r1 != 98) { return 12; };\n"
" if (r2 != 99) { return 13; };\n"
" return 0;\n"
"};\n",
0 },
/* 4. Leaf-only baseline. No nested blocks, no @-prefix scratch
* interaction — a fn that match-binds a `(rune|void)` from a
* concrete scrutinee should frame exactly the bind slot and
* nothing else. Confirms the deferred-prologue refactor didn't
* regress simple frame sizes. */
{ "leaf_baseline",
"package main;\n"
"type tagged = (rune | void);\n"
"fn pickrune(x: tagged) i32 = {\n"
" match (x) {\n"
" case let r: rune => return r: i32;\n"
" case void => return 0;\n"
" };\n"
" return 0;\n"
"};\n"
"export fn main() i32 = {\n"
" let v: tagged = 'A';\n"
" let r: i32 = pickrune(v);\n"
" if (r != 65) { return 11; };\n"
" return 0;\n"
"};\n",
0 },
};
static int
build_with(const char *driver, const char *src_path, const char *tmpdir)
{
char cmd[1024];
snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>/dev/null",
tmpdir, driver, src_path);
return runwait(cmd);
}
static int
exec_bin(const char *bin)
{
return runwait(bin);
}
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 cdrv[640], wdrv[640];
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin);
int have_ww = (access(wdrv, X_OK) == 0);
int n = (int)(sizeof rows / sizeof rows[0]);
int total = 0, fail = 0;
for (int i = 0; i < n; i++) {
const struct row *r = &rows[i];
char src[64], tmpdir[64];
snprintf(src, sizeof src, "/tmp/sz_conv_%d_%d.ww", getpid(), i);
snprintf(tmpdir, sizeof tmpdir, "/tmp/sz_conv_%d_d_%d",
getpid(), i);
FILE *f = fopen(src, "wb");
if (!f) { fail++; total++; continue; }
fputs(r->src, f);
fclose(f);
mkdir(tmpdir, 0755);
const char *base = strrchr(src, '/');
base = base ? base + 1 : src;
char outbin[128];
snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base);
char *dot = strrchr(outbin, '.');
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
/* Runtime parity (cstage). */
total++;
if (build_with(cdrv, src, tmpdir) != 0) {
fprintf(stderr,
"size_strategy_convergence[cs][%s]: build failed\n",
r->label);
fail++;
} else {
int got = exec_bin(outbin);
if (got != r->want) {
fprintf(stderr,
"size_strategy_convergence[cs][%s]: rc=%d want=%d\n",
r->label, got, r->want);
fail++;
}
}
unlink(outbin);
/* Runtime parity (wwstage). */
if (have_ww) {
total++;
if (build_with(wdrv, src, tmpdir) != 0) {
fprintf(stderr,
"size_strategy_convergence[ws][%s]: build failed\n",
r->label);
fail++;
} else {
int got = exec_bin(outbin);
if (got != r->want) {
fprintf(stderr,
"size_strategy_convergence[ws][%s]: rc=%d want=%d\n",
r->label, got, r->want);
fail++;
}
}
unlink(outbin);
}
unlink(src);
rmdir(tmpdir);
}
if (fail) {
fprintf(stderr,
"size_strategy_convergence: %d/%d rows failed\n",
fail, total);
return 1;
}
printf("size_strategy_convergence: %d/%d ok\n", total, total);
return 0;
}