From 5609d0456fb09abe93d0be6f23badcd93c879109 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 19 May 2026 02:13:58 +0900 Subject: [PATCH] selfhost+cstage+test: graduate frame growth to first-use+fail-loud (#15) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Makefile | 7 + cmd/w6c/cgen.c | 102 +- selfhost/cmd/w6c/main.combined.ww | 1088 ++++++---------------- selfhost/cmd/wcc/cgen.ww | 303 +++--- selfhost/cmd/wcc/cgendecl.ww | 676 +------------- selfhost/cmd/wcc/cgenexpr.ww | 47 +- selfhost/cmd/wcc/cgenstmt.ww | 31 +- selfhost/cmd/wcc/cgenutil.ww | 31 +- selfhost/cmd/wwdump/main.combined.ww | 1088 ++++++---------------- test/wcc/748_size_strategy_convergence.c | 293 ++++++ 10 files changed, 1197 insertions(+), 2469 deletions(-) create mode 100644 test/wcc/748_size_strategy_convergence.c diff --git a/Makefile b/Makefile index c53c5430..f168bf2c 100644 --- a/Makefile +++ b/Makefile @@ -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 \ diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index bb5b7e50..ee5211e2 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -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; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 0afd801d..35f0e66b 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -7204,9 +7204,9 @@ fn callee_variadic_param(c: *cgen, callee: *node, nfixed_out: *i32) *node = { // mkvarargname — fresh local-slot name "". 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; @@ -7312,7 +7312,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) { @@ -8880,8 +8880,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, @@ -9252,9 +9252,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 = { @@ -9317,8 +9316,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); @@ -10105,11 +10104,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) { @@ -11426,7 +11425,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); @@ -11493,7 +11492,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); @@ -11937,9 +11936,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); @@ -12449,7 +12448,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; }; @@ -12683,7 +12682,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); @@ -13870,7 +13869,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); @@ -13911,7 +13910,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); @@ -14008,7 +14025,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) { @@ -14017,7 +14034,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 @@ -14171,8 +14188,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 { @@ -14537,12 +14555,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; @@ -16803,13 +16821,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) { @@ -16909,7 +16933,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 @@ -16927,7 +16952,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"); @@ -16956,14 +16981,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) { @@ -16997,7 +17022,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"); @@ -18099,9 +18124,10 @@ fn cgcontinue(c: *cgen, n: *node) void = { // 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 @@ -18117,519 +18143,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_" 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_` - // (counter) and `.rgl_` (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_ for the - // element data and @vararg_sl_ 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 --------------------------------------------- @@ -18639,7 +18152,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 @@ -18952,130 +18465,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 ` 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); @@ -19109,7 +18510,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"); @@ -19117,6 +18518,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 ------------------------------------------------ @@ -19530,6 +18953,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, }; @@ -19575,6 +19005,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, @@ -19604,55 +19045,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, }; @@ -19671,16 +19091,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 @@ -19695,10 +19111,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; }; @@ -19708,6 +19123,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; @@ -19723,6 +19139,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; @@ -19732,86 +19149,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) { @@ -19819,6 +19210,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; }; @@ -19837,21 +19240,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 @@ -19908,7 +19382,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"); }; @@ -20229,7 +19703,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) { @@ -20241,12 +19715,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 @@ -20411,7 +19885,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, @@ -20579,12 +20053,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; @@ -20605,7 +20079,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"); @@ -20616,11 +20090,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); }; }; };}; @@ -20640,7 +20114,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; @@ -20661,7 +20135,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"); @@ -20672,11 +20146,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); }; }; };};};};}; @@ -21044,15 +20518,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 @@ -21063,15 +20537,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 --------------------------------------------------------- diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index 0a50f248..27e76497 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -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 --------------------------------------------------------- diff --git a/selfhost/cmd/wcc/cgendecl.ww b/selfhost/cmd/wcc/cgendecl.ww index afdc411a..9c9cca49 100644 --- a/selfhost/cmd/wcc/cgendecl.ww +++ b/selfhost/cmd/wcc/cgendecl.ww @@ -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_" 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_` - // (counter) and `.rgl_` (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_ for the - // element data and @vararg_sl_ 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 ` 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 ------------------------------------------------ diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index a72f51ca..48761aeb 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -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; diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index 333bd807..fd03fb12 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -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"); diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index c42a5398..3b0a431f 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -71,9 +71,9 @@ fn callee_variadic_param(c: *cgen, callee: *node, nfixed_out: *i32) *node = { // mkvarargname — fresh local-slot name "". 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) { diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index c15e82a1..316c5139 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -7204,9 +7204,9 @@ fn callee_variadic_param(c: *cgen, callee: *node, nfixed_out: *i32) *node = { // mkvarargname — fresh local-slot name "". 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; @@ -7312,7 +7312,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) { @@ -8880,8 +8880,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, @@ -9252,9 +9252,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 = { @@ -9317,8 +9316,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); @@ -10105,11 +10104,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) { @@ -11426,7 +11425,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); @@ -11493,7 +11492,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); @@ -11937,9 +11936,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); @@ -12449,7 +12448,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; }; @@ -12683,7 +12682,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); @@ -13870,7 +13869,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); @@ -13911,7 +13910,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); @@ -14008,7 +14025,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) { @@ -14017,7 +14034,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 @@ -14171,8 +14188,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 { @@ -14537,12 +14555,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; @@ -16803,13 +16821,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) { @@ -16909,7 +16933,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 @@ -16927,7 +16952,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"); @@ -16956,14 +16981,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) { @@ -16997,7 +17022,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"); @@ -18099,9 +18124,10 @@ fn cgcontinue(c: *cgen, n: *node) void = { // 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 @@ -18117,519 +18143,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_" 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_` - // (counter) and `.rgl_` (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_ for the - // element data and @vararg_sl_ 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 --------------------------------------------- @@ -18639,7 +18152,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 @@ -18952,130 +18465,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 ` 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); @@ -19109,7 +18510,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"); @@ -19117,6 +18518,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 ------------------------------------------------ @@ -19530,6 +18953,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, }; @@ -19575,6 +19005,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, @@ -19604,55 +19045,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, }; @@ -19671,16 +19091,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 @@ -19695,10 +19111,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; }; @@ -19708,6 +19123,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; @@ -19723,6 +19139,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; @@ -19732,86 +19149,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) { @@ -19819,6 +19210,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; }; @@ -19837,21 +19240,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 @@ -19908,7 +19382,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"); }; @@ -20229,7 +19703,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) { @@ -20241,12 +19715,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 @@ -20411,7 +19885,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, @@ -20579,12 +20053,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; @@ -20605,7 +20079,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"); @@ -20616,11 +20090,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); }; }; };}; @@ -20640,7 +20114,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; @@ -20661,7 +20135,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"); @@ -20672,11 +20146,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); }; }; };};};};}; @@ -21044,15 +20518,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 @@ -21063,15 +20537,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 --------------------------------------------------------- diff --git a/test/wcc/748_size_strategy_convergence.c b/test/wcc/748_size_strategy_convergence.c new file mode 100644 index 00000000..1385bcca --- /dev/null +++ b/test/wcc/748_size_strategy_convergence.c @@ -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 +#include +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return -1; +} + +struct row { + const char *label; + const char *src; + 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; +}