selfhost+test: single-source-of-truth @tagscr scratch reservation (#38)

Closes STATUS latent #1: @tagscr shared 24B reservation across the
four tagged-scratch sites (cgreturn, pushargsrev, cgindex
tagged-elem, pointer-rooted struct-field tagged write). Any fn that
needed >24B (e.g. slice-in-tagged-field 32B) silently overflowed
into the neighbor frame slot. Surfaced concretely as getopttest's
errortable wwstage exit 16 after #37 fixed the upstream gaps.

c.tagscrsz: i32 on the cgen struct is the single source of truth.
tagscrbump(c, need) in scanlocals raises the max across all 4
reservation sites and returns the frame delta. All emit sites
(cgreturn / pushargsrev / cgindex / cgwidentaggedstore pointer-
rooted) read c.tagscrsz instead of hardcoded 24. Mirrors the existing
cgwidentaggedstore precedent; @tagbase keeps its 8B scanseenmark
dedup (always 8B, correct).

Unmasked latent bug (now fixed): scanlocals's pointer-rooted struct-
field tagged-write detection uses localfindnode(c, base.str) to
resolve the *struct base. For `fn fill(h: *holder)`, h's scan-time
stub from scanseenmark had tnode=nil, so the @tagscr reservation
never fired. Pre-#38 the hardcoded 24B masked this; #38's correctly-
sized slot exposed it. cgfn's param scan loop now sets
c.locals.tnode = scanp.lhs after scanseenmark so localfindnode
resolves param types at scan time.

Test 714 (tagged_return_scratch): 4 rows × 2 stages = 8 fixtures.
Direct adjacency repro; match-arm field-by-field read; **mixed-
sizes-one-fn** (16B pushargsrev widen + 32B cgreturn widen in the
same body — pins the lockstep invariant that a sibling site can't
undersize the shared slot); call-site struct-payload widen. Row 3
specifically would regress if a future refactor ever forgets to
route an emit site through c.tagscrsz.

982 getopt_run green through both stages (was the original surface);
995 self_rebuild byte-id holds.
This commit is contained in:
2026-05-16 13:45:33 +09:00
parent de3bd5cc3b
commit 28f36d84d8
9 changed files with 541 additions and 93 deletions

View File

@@ -416,6 +416,12 @@ type cgen = struct {
// `@vararg_sl_N` using this counter; cgcall resets and walks in
// the same order so the names line up at emission time.
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,
};
// Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar.
@@ -437,6 +443,7 @@ fn cgeninit(c: *cgen, a: *arena) void = {
c.lastwasreturn = 0;
c.labelseq = 0;
c.varargseq = 0;
c.tagscrsz = 0;
// Note: strlit_seq, strlits, ffis are *not* reset here; they
// persist across cgfn calls within one file. cgfile resets them
// at the start of each compilation unit.