w6c+wwstage: aggregate arg from any non-ident source via the closed addr machinery (#271) — close aggregate-arg family

Passing an aggregate BY VALUE as a call argument worked ONLY for a ≤16B
struct from an IDENT source; every non-ident source — CALL mk(), N_DOT
o.f, N_INDEX a[i], DEREF *p — and every array / >24B-struct (even as an
ident) fell to the scalar default: one PUSHQ for a multi-word aggregate,
stack-imbalancing against the type-based multi-word drain. cs!=ww, both
garbage (f(mk()) cs4/ww236, f(o.f) cs8/ww108, f(a[i]) cs4/ww28, f(*p)
cs4/ww140; arrays + 32B sret struct same).

The arg-pass twin of the #265/#268 let-init copy. A new aggregate-arg
push arm materialises the source into the arg convention: the source
ADDRESS in SI (ident LEAQ / deref operand / dotchainaddr #253 /
&base[i] spine #252-270) then its ceil(sz/8) words pushed high→low; a
CALL receives first — ≤24B in AX/DX/CX pushed straight, >24B sret'd
into a per-fn @aggargscr then pushed from there. The pop-forward drain
gained a matching array / >16B-struct arm and the callee prologue an
is_bigagg receive (ceil(sz/8) GP eightbytes), so caller and callee
agree on the multi-word layout. The ≤16B-struct-IDENT fast path is
untouched (byte-id preserved).

The new-arm exclusion is TYPE-keyed (the stamped tinfo, mirroring
cstage node_isstructarg over args[i]->type), not the name-keyed
structparamsize — a name-keyed gate re-opened the #211/#13 cross-module
same-leaf collision (784 symmetric: an 8B `sa.s` struct whose
name-resolution collides with `sb.s = *vtable` would miss the struct
fast path and wrongly enter the new arm, diverging from cstage's
1-word push). A float-bearing ≤16B struct from a non-ident source
loud-stops in both stages (the #165 SSE eightbyte transport the GP
push/drain can't model; out of scope). A const array/struct `def`
global as an aggregate arg is aligned DOWN to the leaner wwstage
(both loud-stop) per rule-10.

#110: cgen is compiler-imported by w6c + wwdump — main.combined.ww
regen'd for both.

949 rows: arg_{struct16,arr16,struct32}_{call,dot,idx,deref,ident},
full member readback (struct 16B reg-class + 32B sret-class + array
[4]u32, each non-ident source + ident control); byteid=1 throughout
(master both-broken-and-divergent → converge on the correct full
push, #263). All 111 dotbaseaddr + 3/3 784 pass; test-unit 241 green;
sizelint + smoke OK; the full w6c compiler source (214705 asm lines)
self-compiles cs==ww byte-id.
This commit is contained in:
2026-06-02 13:42:59 +09:00
parent 3c37b98164
commit 42dd70dc0c
7 changed files with 1204 additions and 38 deletions

View File

@@ -57,6 +57,11 @@ static int cg_tupfscr;
* arg); 0 means "not yet allocated", cg_tupargscr_sz the cached width. */
static int cg_tupargscr;
static int cg_tupargscr_sz;
/* #271: per-fn @aggargscr scratch for a >24B (sret-class) aggregate
* arg sourced from a CALL — the result is sret'd here, then pushed
* word-by-word into the arg convention. 0 = not yet allocated. */
static int cg_aggargscr;
static int cg_aggargscr_sz;
/* Per-fn @-prefix scratch SSoT (task #26, follow-up to #15-cstage's
* @retscr). Pre-#26 each site allocated a labelseq-stamped fresh slot
* per call (mklabel "tagbase" / "tagscr" / "argscr" / "idxscr"); the
@@ -598,6 +603,28 @@ node_isstructarg(Node *n)
return sz > 0 && sz <= 16;
}
/* aggarg_size — byte size of a by-value aggregate (struct OR array)
* call arg, else 0. The size axis the ≤16B-struct node_isstructarg
* carve-out doesn't cover: arrays of any size and structs > 16B (#271).
* Pure-int transport only; a float-bearing struct keeps the #165 SSE
* eightbyte path (gated separately at the push/drain sites). */
static int
aggarg_size(Type *t)
{
if (t == NULL) return 0;
if (t->kind == TY_NAMED) t = t->under;
if (t == NULL) return 0;
if (t->kind == TY_STRUCT || t->kind == TY_ARRAY)
return (int)t->size;
return 0;
}
static int
node_isaggarg(Node *n)
{
return n && aggarg_size(n->type) > 0;
}
/* Pick the appropriate scalar SSE opcode (SS vs SD) for a node's
* float type. Untyped float defaults to SD. */
static int
@@ -1852,6 +1879,70 @@ cg_dotbase_addr(Cg *c, Node *base, int dst_reg, Local *locals)
}
return 1;
}
/* aggarg_srcaddr — land the ADDRESS of an addressable aggregate arg
* source in `dst`, reusing the closed #265/#268 let-init-copy dispatch:
* ident/global slot (LEAQ), deref operand (cgexpr of the pointer),
* N_DOT field (cg_dotchain_addr, #253), N_INDEX element (the &base[i]
* spine, #252/#270). Returns 0 for a source kind not covered (caller
* loud-stops, rule 7). The CALL source is handled separately at the
* push site (receive-to-regs / sret-to-scratch). */
static int
aggarg_srcaddr(Cg *c, Node *src, int dst, Local *locals)
{
if (src->kind == N_UN && src->op == TK_STAR) {
cgexpr(c, src->lhs, locals);
if (dst != D_AX)
ins2(c, A_MOVQ, areg(D_AX), areg(dst));
return 1;
}
if (src->kind == N_IDENT) {
int soff = localfind(locals, src->str);
if (soff != 0) {
ins2(c, A_LEAQ, amem(D_BP, soff), areg(dst));
return 1;
}
/* global value source. Gated to a module-`let` (let_islet,
* the wwstage letvartnode twin); a const array/struct `def`
* aggregate ARG is untested and out of scope (#274) — both
* stages fall through to the caller's loud-stop, aligned DOWN
* to the leaner wwstage per rule-10. */
if (let_islet(src->str)) {
ins2(c, A_LEAQ, masym(c, src->str), areg(dst));
return 1;
}
return 0;
}
if (src->kind == N_DOT)
return cg_dotchain_addr(c, src, dst, locals);
if (src->kind == N_INDEX) {
Node *base = src->lhs;
Node *idx = src->rhs;
Type *bt = base ? base->type : NULL;
Type *bu = (bt && bt->kind == TY_NAMED) ? bt->under : bt;
if (base && base->kind == N_IDENT && bu
&& bu->kind == TY_ARRAY) {
int esz = (bu->sub) ? (int)bu->sub->size : 1;
cgexpr(c, idx, locals);
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_CX));
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
}
int boff = localfind(locals, base->str);
if (boff != 0)
ins2(c, A_LEAQ, amem(D_BP, boff), areg(D_BX));
else
ins2(c, A_LEAQ, masym(c, base->str),
areg(D_BX));
ins2(c, A_ADDQ, areg(D_BX), areg(D_AX));
if (dst != D_AX)
ins2(c, A_MOVQ, areg(D_AX), areg(dst));
return 1;
}
return 0;
}
return 0;
}
/* cg_structlit_fill modes — see helper docstring. */
enum {
DST_BP = 0,
@@ -6397,6 +6488,103 @@ cgexpr(Cg *c, Node *n, Local *locals)
}
continue;
}
/* #271: aggregate (struct/array) arg from any source the
* ≤16B-struct-IDENT fast path above doesn't cover — a
* 16B struct from a non-ident source, OR any array, OR a
* struct > 16B. The arg twin of the #265/#268 let-init
* copy: materialise the source's ADDRESS in SI and push
* its ceil(sz/8) words high→low (the pop drains word0
* into the first arg reg). A CALL source receives first —
* ≤24B in AX/DX/CX pushed straight, >24B sret'd into
* @aggargscr then pushed from there. Pre-fix every such
* source fell to the scalar default (one PUSHQ for a
* multi-word aggregate) and stack-imbalanced against the
* type-based multi-word drain. */
if (!widen[i] && node_isaggarg(args[i])
&& !(node_isstructarg(args[i])
&& args[i]->kind == N_IDENT)) {
int aggsz = aggarg_size(args[i]->type);
int nwords = (aggsz + 7) / 8;
/* A float-bearing ≤16B struct from a non-ident
* source would need the #165 SSE eightbyte
* transport the GP push/drain here can't model —
* loud-stop rather than silently GP-pass it (a
* ≤16B struct with any float field; the wwstage
* tinfo mirror uses the same predicate). */
{
Type *st = args[i]->type;
if (st && st->kind == TY_NAMED)
st = st->under;
if (st && st->kind == TY_STRUCT
&& st->size <= 16) {
int f32;
for (Tfield *f = st->fields; f;
f = f->next)
if (fld_isfloat(f->type,
&f32))
fatal("#271/#165: "
"float-bearing "
"struct arg from a "
"non-ident source "
"needs SSE eightbyte "
"transport (out of "
"scope)");
}
}
if (args[i]->kind == N_CALL) {
if (cg_sret_retsize(args[i]->type) > 0) {
if (cg_aggargscr == 0) {
cg_aggargscr =
local_alloc(c, &locals,
"@aggargscr", aggsz,
cg_frame);
cg_aggargscr_sz = aggsz;
} else if (aggsz >
cg_aggargscr_sz) {
fatal("cgcall: @aggargscr "
"cached sz %d, need %d "
"(#271 pinned offset "
"can't grow)",
cg_aggargscr_sz,
aggsz);
}
cg_sret_dest_off = cg_aggargscr;
cgexpr(c, args[i], locals);
cg_sret_dest_off = 0;
for (int k = nwords - 1; k >= 0;
k--) {
ins2(c, A_MOVQ,
amem(D_BP,
cg_aggargscr + k*8),
areg(D_AX));
ins1(c, A_PUSHQ,
areg(D_AX));
}
} else {
/* ≤24B: producer left AX=word0,
* DX=word1, CX=word2. Push
* high→low so the pop drains
* word0 first. */
int rr[3] = { D_AX, D_DX, D_CX };
cgexpr(c, args[i], locals);
for (int k = nwords - 1; k >= 0;
k--)
ins1(c, A_PUSHQ,
areg(rr[k]));
}
continue;
}
if (!aggarg_srcaddr(c, args[i], D_SI, locals))
fatal("#271: aggregate arg from "
"unsupported source kind %d",
args[i]->kind);
for (int k = nwords - 1; k >= 0; k--) {
ins2(c, A_MOVQ, amem(D_SI, k*8),
areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
}
continue;
}
if (widen[i]) {
/* Concrete → tagged-union widening at the call
* site. Mirrors the let/assign/return widening:
@@ -6687,6 +6875,24 @@ cgexpr(Cg *c, Node *n, Local *locals)
stackslots++;
}
}
} else if (node_isaggarg(args[i])
&& !node_isstructarg(args[i])) {
/* #271: array / >16B-struct aggregate arg —
* drain its ceil(sz/8) staged words into the
* INTEGER arg cursor (overflow spills to the
* stack, reached by the callee via positive BP
* offsets). The ≤16B struct case stays in
* node_isstructarg above (SSE class path
* intact). */
int aggsz = aggarg_size(args[i]->type);
int nw = (aggsz + 7) / 8;
for (int k = 0; k < nw; k++) {
if (ii < 6)
ins1(c, A_POPQ,
areg(sysv_argregs[ii++]));
else
stackslots++;
}
} else if (node_istaggedarg(args[i])) {
int sz = tagged_arg_size(args[i]->type);
int eb = sz / 8;
@@ -10284,6 +10490,8 @@ cgfn(Cg *c, FILE *out, Node *fn)
cg_tupfscr = 0;
cg_tupargscr = 0;
cg_tupargscr_sz = 0;
cg_aggargscr = 0;
cg_aggargscr_sz = 0;
cg_tagbase = 0;
cg_tagbase_sz = 0;
cg_tagscr = 0;
@@ -10343,6 +10551,13 @@ cgfn(Cg *c, FILE *out, Node *fn)
int slice = (pu && pu->kind == TY_SLICE);
int is_str = type_isstr(pt);
int is_struct = pu && pu->kind == TY_STRUCT && pu->size <= 16;
/* #271: a by-value array param, or a struct param > 16B —
* received as ceil(sz/8) GP eightbytes, the callee twin of the
* generalised aggregate-arg push. The ≤16B struct keeps its own
* (possibly SSE-classified) path above. */
int is_bigagg = pu && ((pu->kind == TY_ARRAY)
|| (pu->kind == TY_STRUCT && pu->size > 16));
int agg_eb = is_bigagg ? (int)((pu->size + 7) / 8) : 0;
int tagged_sz = tagged_arg_size(pt);
int is_tagged = tagged_sz > 0;
int isf = cg_isfloat(pt);
@@ -10448,7 +10663,8 @@ cgfn(Cg *c, FILE *out, Node *fn)
* — the caller pushes the triple (#1/Phase 3). */
int eightbytes = (slice || is_str) ? 3 :
(is_struct ? struct_eb :
(is_tagged ? tagged_eb : 1));
(is_bigagg ? agg_eb :
(is_tagged ? tagged_eb : 1)));
int regs_left = isf ? (8 - fargi) : (6 - argi);
if (regs_left >= eightbytes) {
/* #60: route slice/str slot widths through Type.size SSoT
@@ -10456,9 +10672,10 @@ cgfn(Cg *c, FILE *out, Node *fn)
* this site (or its stack-stitch mirror below). */
int sz = (slice || is_str) ? (int)pu->size :
(is_struct ? (int)pu->size :
(is_tagged ? tagged_sz : 8));
(is_bigagg ? (int)pu->size :
(is_tagged ? tagged_sz : 8)));
int off = localoff(c, &locals, p->str, sz, &frame);
if (slice || is_str || is_struct || is_tagged) {
if (slice || is_str || is_struct || is_bigagg || is_tagged) {
for (int k = 0; k < eightbytes; k++, argi++)
ins2(c, A_MOVQ,
areg(sysv_argregs[argi]),
@@ -10476,7 +10693,7 @@ cgfn(Cg *c, FILE *out, Node *fn)
argi++;
}
} else if (eightbytes > 1 && regs_left > 0 &&
(slice || is_str || is_struct || is_tagged)) {
(slice || is_str || is_struct || is_bigagg || is_tagged)) {
/* Multi-word arg that partially fits in regs: caller
* filled (regs_left) registers greedily, the rest spilled
* to stack at positive BP offsets. Stitch a single local
@@ -10486,7 +10703,8 @@ cgfn(Cg *c, FILE *out, Node *fn)
/* #60: same SSoT routing as the regs-fit arm above. */
int sz = (slice || is_str) ? (int)pu->size :
(is_struct ? (int)pu->size :
(is_tagged ? tagged_sz : 8));
(is_bigagg ? (int)pu->size :
(is_tagged ? tagged_sz : 8)));
int off = localoff(c, &locals, p->str, sz, &frame);
extern int cg_stack_arg_cursor;
int k = 0;