selfhost+test: bump wwstage varargseq per cgcall (#8)

Latent surface from #15: cgcall variadic-gather block read seq from
n.uval, which post-#15 is always 0 because scanlocals (which used to
stamp it during pre-pass) was deleted. Every variadic callsite in a
fn aliased to @vararg_d_0 / @vararg_sl_0. When two callsites in one
fn had differing arities, the second hit #15's first-use+fail-loud
guard ("localadd: @-prefix slot grew within fn") — correctly, since
the slot was being asked to grow mid-fn.

Fix: read seq from c.varargseq + bump in cgcall's gather branch.
Mirrors cstage's mklabel("vararg_d/sl") natural seq bump.
cgeninit zeroes c.varargseq per-fn (existing), so the counter is
correctly per-fn scoped.

cgen.ww varargseq comment refreshed — replaces stale "bumped only at
emit time" misclaim with the post-#15 per-call shape + the #15
grow-on-pin discipline that surfaced the wedge.

751_vararg_seq_percall: table-driven 3 rows x 2 stages = 6 fixtures.
mixed_arity_two_calls (the wedge), same_arity_two_calls (no-regress),
three_arity_drift (1/2/3 mints @vararg_d_0/1/2).

make test 125/125; ww2==ww3==ww4 byte-id holds via 995_self_rebuild.

Surfaced by worker-strcontains2 attempting strings.contains tagged-
variadic graduation — mixed-arity spec test rows triggered the wedge.
Unblocks #9 + #10 (strings/bytes.contains).
This commit is contained in:
2026-05-19 04:04:41 +09:00
parent d2c64bc962
commit 5ed6293330
6 changed files with 238 additions and 21 deletions

View File

@@ -13906,8 +13906,9 @@ fn cgcall(c: *cgen, n: *node) void = {
// of the call machinery sees one slice slot for the variadic.
// 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 cgcall stamped on n.uval at first emit.
// (no trailing args) writes a {nil, 0, 0} descriptor. Per-call
// seq comes from c.varargseq bumped at gather emit (mirrors
// cstage's mklabel("vararg_d/sl") freshness).
{
let nfixed_v: i32 = 0;
let varp: *node = callee_variadic_param(c, callee, &nfixed_v);
@@ -13945,7 +13946,8 @@ fn cgcall(c: *cgen, n: *node) void = {
if (prev == nil) { n.list = inner; }
else { prev.next = inner; };
} else {
let seq: i32 = n.uval: i32;
let seq: i32 = c.varargseq;
c.varargseq += 1;
let dname: str = mkvarargname(c, "@vararg_d_", seq);
let sname: str = mkvarargname(c, "@vararg_sl_", seq);
// Use raw element size, not stack-padded
@@ -19083,10 +19085,12 @@ 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. 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).
// Variadic-call gather state. cgcall bumps this on each gather
// emit and uses it to mint `@vararg_d_N` / `@vararg_sl_N` per
// callsite; mirrors cstage's mklabel("vararg_d/sl") freshness
// so two variadic callsites with different arities in one fn
// get distinct slots (the shared slot fail-louds under #15's
// @-prefix grow-on-pin discipline).
varargseq: i32,
// System V AMD64 sret discipline (#23). Plain TY_STRUCT returns
// with size > 24B are passed via a hidden first-arg pointer

View File

@@ -469,10 +469,12 @@ 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. 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).
// Variadic-call gather state. cgcall bumps this on each gather
// emit and uses it to mint `@vararg_d_N` / `@vararg_sl_N` per
// callsite; mirrors cstage's mklabel("vararg_d/sl") freshness
// so two variadic callsites with different arities in one fn
// get distinct slots (the shared slot fail-louds under #15's
// @-prefix grow-on-pin discipline).
varargseq: i32,
// System V AMD64 sret discipline (#23). Plain TY_STRUCT returns
// with size > 24B are passed via a hidden first-arg pointer

View File

@@ -2940,8 +2940,9 @@ fn cgcall(c: *cgen, n: *node) void = {
// of the call machinery sees one slice slot for the variadic.
// 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 cgcall stamped on n.uval at first emit.
// (no trailing args) writes a {nil, 0, 0} descriptor. Per-call
// seq comes from c.varargseq bumped at gather emit (mirrors
// cstage's mklabel("vararg_d/sl") freshness).
{
let nfixed_v: i32 = 0;
let varp: *node = callee_variadic_param(c, callee, &nfixed_v);
@@ -2979,7 +2980,8 @@ fn cgcall(c: *cgen, n: *node) void = {
if (prev == nil) { n.list = inner; }
else { prev.next = inner; };
} else {
let seq: i32 = n.uval: i32;
let seq: i32 = c.varargseq;
c.varargseq += 1;
let dname: str = mkvarargname(c, "@vararg_d_", seq);
let sname: str = mkvarargname(c, "@vararg_sl_", seq);
// Use raw element size, not stack-padded

View File

@@ -13906,8 +13906,9 @@ fn cgcall(c: *cgen, n: *node) void = {
// of the call machinery sees one slice slot for the variadic.
// 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 cgcall stamped on n.uval at first emit.
// (no trailing args) writes a {nil, 0, 0} descriptor. Per-call
// seq comes from c.varargseq bumped at gather emit (mirrors
// cstage's mklabel("vararg_d/sl") freshness).
{
let nfixed_v: i32 = 0;
let varp: *node = callee_variadic_param(c, callee, &nfixed_v);
@@ -13945,7 +13946,8 @@ fn cgcall(c: *cgen, n: *node) void = {
if (prev == nil) { n.list = inner; }
else { prev.next = inner; };
} else {
let seq: i32 = n.uval: i32;
let seq: i32 = c.varargseq;
c.varargseq += 1;
let dname: str = mkvarargname(c, "@vararg_d_", seq);
let sname: str = mkvarargname(c, "@vararg_sl_", seq);
// Use raw element size, not stack-padded
@@ -19083,10 +19085,12 @@ 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. 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).
// Variadic-call gather state. cgcall bumps this on each gather
// emit and uses it to mint `@vararg_d_N` / `@vararg_sl_N` per
// callsite; mirrors cstage's mklabel("vararg_d/sl") freshness
// so two variadic callsites with different arities in one fn
// get distinct slots (the shared slot fail-louds under #15's
// @-prefix grow-on-pin discipline).
varargseq: i32,
// System V AMD64 sret discipline (#23). Plain TY_STRUCT returns
// with size > 24B are passed via a hidden first-arg pointer