w6c+w6c_ww: tagged sret for slot>32B returns (fix #38)
A tagged-union RETURN rides a fixed AX(tag)+DX/CX/R8 cursor (TUPLE_GPCAP eightbytes = 32B slot); wider slots were silently truncated at the return crossing — payload word 4+ built in the callee frame and died there, byte-identical on both stages (gate-blind). Blocks regex fold-2a ((regex | error | nomem) = 64B slot). Classifier: cg_sret_retsize / sretretsize gain a TY_TAGGED arm (<= TUPLE_GPCAP*8 stays register-ABI — the (str|nomem)/(s3|bool) 32B boundary class is pinned unchanged byte-for-byte vs master). Callee: cgreturn writes the slot through *(@sretarg) via the existing widener non-BP base (bare return stores the void tag); exact-type 'return f();' rides the #9 sret-forward. Receive: let/assign/discard reuse the generic #23/#10 sret protocol; the match scrutinee passes its spill slot as the sret dest (tagged-specific, no tuple precedent). This could NOT land as a gate-first interim loud-stop (the planned #38a): lib/errors/errors.ww errno() already returns a 40B (errors.error) slot in-tree — the cgenstmt.ww-documented #222 latent — so a bare gate breaks the build. errno graduates to sret here instead; errnotest pins it at runtime (its cstage run; the wwstage run was already failing at master via an unrelated pre-existing indirect-call arg-classification divergence, reported separately) and test/926's errno-shaped row reads the previously-dropped tail word on both stages. The unwired cursor consumers of an sret-class call result loud-stop (rule 7) rather than read a cursor the callee no longer fills: widening forward/receive ((A|B)->(A|B|C) mem-to-mem tag-remap, filed #40), ?/!/is/as operands, argument position, and the >48B tagged-arg class both stages previously mishandled silently. One-class-one-commit per the #133 carve-out: post-flip those consumers would read AX (now the dest pointer) as the tag — a gates-trailing commit would leave a silently-wrong bisect point, so the flip and its gates are not separable. test/926: 15 rows — 56B regex-shaped round-trips (literal/local/ assign/match-scrutinee/forward/str-variant/multi-call), 40B repro + bare-return-void, the errno-shaped tail-read graduation row, 32B boundary rows pinned register-ABI by asm sentinel, and 3 loud-stop rows pinned as build failures on both stages.
This commit is contained in:
232
cmd/w6c/cgen.c
232
cmd/w6c/cgen.c
@@ -322,6 +322,15 @@ cg_sret_retsize(Type *rt)
|
||||
if (rt == NULL) return 0;
|
||||
if (rt->kind == TY_STRUCT)
|
||||
return (int)rt->size <= 24 ? 0 : (int)rt->size;
|
||||
/* #38: a tagged union rides AX(tag)+DX/CX/R8 = TUPLE_GPCAP
|
||||
* eightbytes; a wider slot was silently truncated (payload word
|
||||
* 4+ died in the callee frame). The ≤cap boundary is load-bearing:
|
||||
* (str|nomem)-shaped 32B slots MUST stay register-ABI or every
|
||||
* such consumer in the tree flips. Nullable folds to one word. */
|
||||
if (rt->kind == TY_TAGGED) {
|
||||
if (rt->nullable) return 0;
|
||||
return (int)rt->size <= TUPLE_GPCAP * 8 ? 0 : (int)rt->size;
|
||||
}
|
||||
/* #267: arrays ride the struct-return ABI — same ≤24 reg / >24 sret
|
||||
* split. Pure-int element arrays only; no float-array-return
|
||||
* consumer exists, so struct_float_class stays struct-only. */
|
||||
@@ -2163,6 +2172,16 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src,
|
||||
amem(D_BP, write_off + 8 + k));
|
||||
}
|
||||
} else {
|
||||
/* #38b: an sret-classified call result is in
|
||||
* memory (AX = dest pointer), not the cursor —
|
||||
* the spill below would store the pointer as
|
||||
* the payload. Mem-to-mem widen is #40. */
|
||||
if (src->kind == N_CALL
|
||||
&& cg_sret_retsize(st) > 0)
|
||||
fatal("#40: sret-class call result "
|
||||
"cannot be cursor-widened into a "
|
||||
"tagged slot (mem-to-mem widen "
|
||||
"unwired)");
|
||||
cgexpr(c, src, *locals_p);
|
||||
ins2(c, A_MOVQ, areg(D_AX),
|
||||
amem(D_BP, write_off + 8));
|
||||
@@ -2196,6 +2215,12 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src,
|
||||
* variant-widen) so the unconditional store here is
|
||||
* safe even when the source variant has fewer payload
|
||||
* words than the dst slot. */
|
||||
/* #38b: an sret-classified call result is in memory
|
||||
* (AX = dest pointer), not the cursor. #40. */
|
||||
if (src->kind == N_CALL && cg_sret_retsize(st) > 0)
|
||||
fatal("#40: sret-class call result cannot be "
|
||||
"cursor-widened into a tagged slot "
|
||||
"(mem-to-mem widen unwired)");
|
||||
cgexpr(c, src, *locals_p);
|
||||
ins2(c, A_MOVQ, areg(D_AX),
|
||||
amem(D_BP, write_off + 0));
|
||||
@@ -2345,7 +2370,7 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src,
|
||||
? ftype->under : ftype;
|
||||
/* str IS []u8 and a slice is the same 3-word
|
||||
* {ptr,len,cap} header from cgexpr's AX/BX/CX
|
||||
* (#1/Phase 3). The slice arm rides #38's
|
||||
* (#1/Phase 3). The slice arm rides #38b's
|
||||
* regex-shaped consumer (slice fields inside a
|
||||
* union-payload struct literal); the prior
|
||||
* str-only gate dropped .len/.cap via the
|
||||
@@ -5626,17 +5651,36 @@ cgexpr(Cg *c, Node *n, Local *locals)
|
||||
/* Plain `r = expr;` where r is a tagged-union local.
|
||||
* Delegates to cg_widen_tagged_store: covers nullable fold,
|
||||
* tagged→tagged (with tag remap), struct payload (ident or
|
||||
* literal), str payload, and scalar payload. */
|
||||
* literal), str payload, and scalar payload.
|
||||
*
|
||||
* #38b: an sret-classified tagged CALL result is in memory,
|
||||
* not the cursor — an exact-type reassign falls through to
|
||||
* the generic sret receive below; a widening receive needs
|
||||
* mem-to-mem tag-remap (#40, unwired). */
|
||||
if (n->lhs && n->lhs->kind == N_IDENT && n->op == TK_ASSIGN
|
||||
&& n->lhs->type) {
|
||||
Type *lt = n->lhs->type;
|
||||
Type *lu = (lt && lt->kind == TY_NAMED) ? lt->under : lt;
|
||||
if (lu && lu->kind == TY_TAGGED) {
|
||||
int off = localfind(locals, n->lhs->str);
|
||||
if (off == 0) break;
|
||||
cg_widen_tagged_store(c, &locals, lu, n->rhs,
|
||||
D_BP, off, (int)lu->size);
|
||||
break;
|
||||
int rhs_sret_call = n->rhs
|
||||
&& n->rhs->kind == N_CALL
|
||||
&& cg_sret_retsize(n->rhs->type) > 0;
|
||||
if (!rhs_sret_call) {
|
||||
int off = localfind(locals,
|
||||
n->lhs->str);
|
||||
if (off == 0) break;
|
||||
cg_widen_tagged_store(c, &locals, lu,
|
||||
n->rhs, D_BP, off, (int)lu->size);
|
||||
break;
|
||||
}
|
||||
Type *ru = type_chase_named(n->rhs->type);
|
||||
if (!(ru == lu || type_eq(n->rhs->type, lt)))
|
||||
fatal("#40: sret-class call result "
|
||||
"cannot be widened into a tagged "
|
||||
"slot (mem-to-mem widen unwired)");
|
||||
if (localfind(locals, n->lhs->str) == 0)
|
||||
fatal("#38b: sret receive into a "
|
||||
"tagged GLOBAL lvalue unwired");
|
||||
}
|
||||
}
|
||||
/* Deref-target assignment `*p = v;`. The size of the store is
|
||||
@@ -7020,6 +7064,20 @@ cgexpr(Cg *c, Node *n, Local *locals)
|
||||
args[i], widen_sz[i]);
|
||||
continue;
|
||||
}
|
||||
/* #38b residual (rule 7): a tagged arg slot past the
|
||||
* 6-reg arg capacity has no push shape —
|
||||
* tagged_arg_size returns 0 ("too large") and the
|
||||
* scalar default silently pushed ONE word. Loud-stop;
|
||||
* symmetric ww gate in pushargsrev. */
|
||||
{
|
||||
Type *au = type_chase_named(args[i]->type);
|
||||
if (au && au->kind == TY_TAGGED
|
||||
&& !au->nullable
|
||||
&& tagged_arg_size(args[i]->type) == 0)
|
||||
fatal("#38b: tagged arg exceeds the "
|
||||
"register arg capacity (>48B "
|
||||
"slot) — unwired");
|
||||
}
|
||||
cgexpr(c, args[i], locals);
|
||||
Type *tuparg_push = node_tuplearg(args[i]);
|
||||
if (node_isfloat(args[i])) {
|
||||
@@ -7052,6 +7110,15 @@ cgexpr(Cg *c, Node *n, Local *locals)
|
||||
* values into arg-reg[1..]. Nullable (sz=8):
|
||||
* AX holds the pointer, no value-word
|
||||
* registers — push just AX. */
|
||||
/* #38b residual (rule 7): an sret-class call
|
||||
* result is in memory, not the cursor — the
|
||||
* @aggargscr-style receive-then-push is the
|
||||
* #40-family follow-up. */
|
||||
if (args[i]->kind == N_CALL
|
||||
&& cg_sret_retsize(args[i]->type) > 0)
|
||||
fatal("#38b: >32B tagged call result "
|
||||
"as a call argument unwired "
|
||||
"(#40-family follow-up)");
|
||||
int sz = tagged_arg_size(args[i]->type);
|
||||
if (sz > 24)
|
||||
ins1(c, A_PUSHQ, areg(D_R8));
|
||||
@@ -7517,6 +7584,16 @@ cgexpr(Cg *c, Node *n, Local *locals)
|
||||
* spill only that. */
|
||||
sl_off = localoff(c, &locals, "@match_spill", slot_size,
|
||||
cg_frame);
|
||||
if (s->kind == N_CALL && cg_sret_retsize(st) > 0) {
|
||||
/* #38b: sret-classified tagged call — pass the
|
||||
* scrut slot itself as the sret dest and skip
|
||||
* the cursor spill; downstream tag dispatch /
|
||||
* case-let binds already read the slot from
|
||||
* memory. */
|
||||
cg_sret_dest_off = sl_off;
|
||||
cgexpr(c, s, locals);
|
||||
cg_sret_dest_off = 0;
|
||||
} else {
|
||||
cgexpr(c, s, locals);
|
||||
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, sl_off + 0));
|
||||
if (!is_nullable) {
|
||||
@@ -7529,6 +7606,7 @@ cgexpr(Cg *c, Node *n, Local *locals)
|
||||
ins2(c, A_MOVQ, areg(D_R8),
|
||||
amem(D_BP, sl_off + 24));
|
||||
}
|
||||
}
|
||||
}
|
||||
char *end = mklabel(c, "match_end");
|
||||
/* Push the end label as the yield target for arm bodies. */
|
||||
@@ -7648,6 +7726,20 @@ cgexpr(Cg *c, Node *n, Local *locals)
|
||||
* (any non-null), void variant is the error (null). The
|
||||
* enclosing fn's null encoding is the same — RET with AX=0
|
||||
* if propagating; otherwise leave AX as-is on success. */
|
||||
/* #38b residuals (rule 7): the cursor read below cannot see
|
||||
* an sret-classified call result (AX = dest pointer), and the
|
||||
* propagate-RET below cannot speak an sret-classified
|
||||
* enclosing return (the caller reads memory, not the
|
||||
* cursor). Both are unwired follow-ups of #40's family. */
|
||||
if (n->lhs && n->lhs->kind == N_CALL
|
||||
&& cg_sret_retsize(n->lhs->type) > 0)
|
||||
fatal("#38b: `?` on an sret-class call result "
|
||||
"unwired (mem-based unwrap is a #40-family "
|
||||
"follow-up)");
|
||||
if (cg_sret_retsize(cg_ret_type) > 0)
|
||||
fatal("#38b: `?` propagation into a >32B tagged "
|
||||
"return unwired (sret error-propagate is a "
|
||||
"#40-family follow-up)");
|
||||
cgexpr(c, n->lhs, locals);
|
||||
Type *u = n->lhs ? n->lhs->type : NULL;
|
||||
if (u && u->kind == TY_NAMED) u = u->under;
|
||||
@@ -7728,6 +7820,12 @@ cgexpr(Cg *c, Node *n, Local *locals)
|
||||
/* On error variant: exit(1) directly via the syscall.
|
||||
* Nullable: null = error; non-null = success (AX is the
|
||||
* pointer, ready to use). */
|
||||
/* #38b residual (rule 7): see the N_TRYPROP twin. */
|
||||
if (n->lhs && n->lhs->kind == N_CALL
|
||||
&& cg_sret_retsize(n->lhs->type) > 0)
|
||||
fatal("#38b: `!` on an sret-class call result "
|
||||
"unwired (mem-based unwrap is a #40-family "
|
||||
"follow-up)");
|
||||
cgexpr(c, n->lhs, locals);
|
||||
Type *u = n->lhs ? n->lhs->type : NULL;
|
||||
if (u && u->kind == TY_NAMED) u = u->under;
|
||||
@@ -7779,6 +7877,13 @@ cgexpr(Cg *c, Node *n, Local *locals)
|
||||
/* `e is T` — Compare scrutinee tag against T's variant index.
|
||||
* Result is bool (0/1) in AX. Nullable: discriminator is
|
||||
* pointer-vs-null, not a tag. */
|
||||
/* #38b residual (rule 7): an sret-class call result leaves
|
||||
* AX = dest pointer, not the tag — mem-based test is a
|
||||
* #40-family follow-up. */
|
||||
if (n->lhs && n->lhs->kind == N_CALL
|
||||
&& cg_sret_retsize(n->lhs->type) > 0)
|
||||
fatal("#38b: `is` on an sret-class call result "
|
||||
"unwired (#40-family follow-up)");
|
||||
cgexpr(c, n->lhs, locals);
|
||||
Type *u = n->lhs ? n->lhs->type : NULL;
|
||||
if (u && u->kind == TY_NAMED) u = u->under;
|
||||
@@ -7835,6 +7940,11 @@ cgexpr(Cg *c, Node *n, Local *locals)
|
||||
}
|
||||
}
|
||||
int slot_size = (u && u->kind == TY_TAGGED) ? (int)u->size : 16;
|
||||
/* #38b residual (rule 7): the @asrt_spill below reads the
|
||||
* cursor, which an sret-class call result never fills. */
|
||||
if (s && s->kind == N_CALL && cg_sret_retsize(st) > 0)
|
||||
fatal("#38b: `as` on an sret-class call result "
|
||||
"unwired (#40-family follow-up)");
|
||||
int sl_off = 0;
|
||||
if (s && s->kind == N_IDENT && s->str) {
|
||||
sl_off = localfind(locals, s->str);
|
||||
@@ -9327,14 +9437,43 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
|
||||
}
|
||||
break;
|
||||
}
|
||||
/* #38b residual (rule 7): `let w: T = f()?;` / `f()!` where f
|
||||
* returns an sret-classified tagged union — the unwrap would
|
||||
* need a mem-based read of the sret slot. The N_LET arms
|
||||
* below have no TRYUNW/TRYPROP shape for a >8B lt, so the
|
||||
* rhs was SILENTLY dropped (no CALL emitted; wwstage's cglet
|
||||
* default does cgexpr and hits the cgtryunw/cgtryprop gates —
|
||||
* this keeps acceptance symmetric, rule 10). */
|
||||
if (n->rhs
|
||||
&& (n->rhs->kind == N_TRYUNW || n->rhs->kind == N_TRYPROP)
|
||||
&& n->rhs->lhs && n->rhs->lhs->kind == N_CALL
|
||||
&& cg_sret_retsize(n->rhs->lhs->type) > 0)
|
||||
fatal("#38b: `?`/`!` on an sret-class call result "
|
||||
"unwired (mem-based unwrap is a #40-family "
|
||||
"follow-up)");
|
||||
/* Tagged-union initialiser. Delegates to cg_widen_tagged_store,
|
||||
* which handles nullable fold, tagged→tagged (with tag remap
|
||||
* when variant indices differ), struct payload (ident or
|
||||
* literal — field-by-field at slot+8+field_off), str payload,
|
||||
* and scalar payload (with zero-pad to the slot size). */
|
||||
* and scalar payload (with zero-pad to the slot size).
|
||||
*
|
||||
* #38b: an sret-classified tagged CALL result is in memory,
|
||||
* not the cursor — an exact-type receive falls through to the
|
||||
* generic sret receive below (the let's slot IS the dest); a
|
||||
* widening receive needs mem-to-mem tag-remap (#40, unwired). */
|
||||
if (n->rhs && lu && lu->kind == TY_TAGGED) {
|
||||
cg_widen_tagged_store(c, locals, lu, n->rhs, D_BP, off, sz);
|
||||
break;
|
||||
int rhs_sret_call = n->rhs->kind == N_CALL
|
||||
&& cg_sret_retsize(n->rhs->type) > 0;
|
||||
if (!rhs_sret_call) {
|
||||
cg_widen_tagged_store(c, locals, lu, n->rhs,
|
||||
D_BP, off, sz);
|
||||
break;
|
||||
}
|
||||
Type *ru = type_chase_named(n->rhs->type);
|
||||
if (!(ru == lu || type_eq(n->rhs->type, lt)))
|
||||
fatal("#40: sret-class call result cannot be "
|
||||
"widened into a tagged slot (mem-to-mem "
|
||||
"widen unwired)");
|
||||
}
|
||||
/* Every slice initialiser routes here — fn-return, slice
|
||||
* ident, slice param, and sub-slice `buf[lo:hi]`. cgexpr
|
||||
@@ -9749,6 +9888,27 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
|
||||
Type *rt = cg_ret_type;
|
||||
if (rt->kind == TY_NAMED) rt = rt->under;
|
||||
if (rt && rt->kind == TY_TAGGED) {
|
||||
/* #38b: an sret-classified tagged return (slot
|
||||
* > the AX/DX/CX/R8 cursor) writes the void-
|
||||
* variant tag through *(@sretarg) and returns
|
||||
* the dest pointer — the cursor can't carry the
|
||||
* slot and the caller reads memory. */
|
||||
if (cg_sret_retsize(rt) > 0) {
|
||||
int tag = cg_tag_for_variant(rt, ty_void);
|
||||
if (tag < 0) tag = 0;
|
||||
ins2(c, A_MOVQ,
|
||||
amem(D_BP, cg_sret_arg_off),
|
||||
areg(D_BX));
|
||||
ins2(c, A_MOVQ, aimm(tag),
|
||||
amem(D_BX, 0));
|
||||
ins2(c, A_MOVQ,
|
||||
amem(D_BP, cg_sret_arg_off),
|
||||
areg(D_AX));
|
||||
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
|
||||
ins1(c, A_POPQ, areg(D_BP));
|
||||
ins0(c, A_RET);
|
||||
break;
|
||||
}
|
||||
if (rt->nullable) {
|
||||
/* bare `return;` is the void/null
|
||||
* variant: emit AX = 0. */
|
||||
@@ -9796,6 +9956,58 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
|
||||
* arm below zeroed the whole value (never packed the
|
||||
* operands). */
|
||||
int istuple = vu && vu->kind == TY_TUPLE;
|
||||
/* #38b: sret-classified tagged return (slot >
|
||||
* the AX/DX/CX/R8 cursor). Three shapes:
|
||||
* - exact-type N_CALL forward: inner sret's
|
||||
* straight into outer's caller dest (#9
|
||||
* shape, cg_sret_forward).
|
||||
* - widening from an sret-class tagged source
|
||||
* ((A|B)→(A|B|C) mem-to-mem tag-remap):
|
||||
* unwired, loud-stop — #40.
|
||||
* - everything else: cg_widen_tagged_store
|
||||
* through *(@sretarg) (the widener already
|
||||
* speaks non-BP bases, the #34 precedent),
|
||||
* then return the dest pointer. */
|
||||
if (cg_sret_retsize(rt) > 0) {
|
||||
int sz = (int)rt->size;
|
||||
if (passthrough) {
|
||||
/* exact type but a cursor source
|
||||
* (N_INDEX/N_DOT) can't carry
|
||||
* >32B — loud-stop (rule 7,
|
||||
* #38b residual). */
|
||||
if (n->lhs->kind != N_CALL)
|
||||
fatal("#38b: >32B tagged "
|
||||
"return from a cursor "
|
||||
"source (kind %d) "
|
||||
"unsupported",
|
||||
n->lhs->kind);
|
||||
cg_sret_forward = 1;
|
||||
cgexpr(c, n->lhs, *locals);
|
||||
ins2(c, A_MOVQ,
|
||||
amem(D_BP, cg_sret_arg_off),
|
||||
areg(D_AX));
|
||||
} else if (istagged
|
||||
&& n->lhs->kind != N_IDENT
|
||||
&& (int)vu->size > TUPLE_GPCAP * 8) {
|
||||
fatal("#40: widening tagged "
|
||||
"return-forward of a >32B "
|
||||
"source needs mem-to-mem "
|
||||
"tag-remap (unwired)");
|
||||
} else {
|
||||
ins2(c, A_MOVQ,
|
||||
amem(D_BP, cg_sret_arg_off),
|
||||
areg(D_BX));
|
||||
cg_widen_tagged_store(c, locals,
|
||||
rt, n->lhs, D_BX, 0, sz);
|
||||
ins2(c, A_MOVQ,
|
||||
amem(D_BP, cg_sret_arg_off),
|
||||
areg(D_AX));
|
||||
}
|
||||
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
|
||||
ins1(c, A_POPQ, areg(D_BP));
|
||||
ins0(c, A_RET);
|
||||
break;
|
||||
}
|
||||
if (rt->nullable) {
|
||||
cgexpr(c, n->lhs, *locals);
|
||||
} else if (passthrough) {
|
||||
|
||||
Reference in New Issue
Block a user