w6c: materialize full tagged slot on N_IDENT-source return (#263) — cstage align-up to wwstage

cgreturn's passthrough predicate was TYPE-only (istagged && type-eq), with
no source-kind filter. It forwarded the source's AX/DX/CX unchanged, which
is correct ONLY when the source already materialized the full tagged slot
into registers — N_CALL / N_INDEX / N_DOT (the #261-broadened set). For a
tagged LOCAL ident, cgexpr loads only word0 (the tag) into AX, never the
payload into DX, so passthrough dropped the payload: `return v` of a
`(i32|void)=7i32` exited 0 instead of 7. wwstage was already correct — its
forwardtagged kind filter excludes N_IDENT, routing it through the
scratch-widen path. The runtime oracle (cstage 0, wwstage 7) proved cstage
is the bug; this aligns cstage UP.

Gate passthrough to {N_CALL,N_INDEX,N_DOT}; a tagged-ident return now falls
to the existing scratch-slot widen path (cg_widen_tagged_store tagged-subset
N_IDENT arm), byte-identical to wwstage's return scratch-widen. cstage-only
(no combined.ww regen — combined.ww embeds the unchanged wwstage source;
byte-id is blind here, the new 949 rows are the net).

test/949: tagged_ident_ret_i32 (7) + tagged_ident_ret_void (void tag
survives) + register-resident controls tagged_call_ret_ctrl /
tagged_dot_ret_ctrl (passthrough must still fire); INDEX control already
present. All dual-stage run + cs==ww byte-id.
This commit is contained in:
2026-06-02 06:28:00 +09:00
parent 6d8434002d
commit 0afe4225cd
2 changed files with 73 additions and 1 deletions

View File

@@ -8843,7 +8843,21 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
Type *vu = (vt && vt->kind == TY_NAMED)
? vt->under : vt;
int istagged = vu && vu->kind == TY_TAGGED;
int passthrough = istagged && (vu == rt ||
/* #263: passthrough forwards the source's AX/DX/CX
* unchanged — correct ONLY when the source already
* materialised the FULL tagged slot into registers:
* N_CALL / N_INDEX / N_DOT (the #261-broadened set).
* A tagged LOCAL ident leaves only word0 (the tag)
* in AX (cgexpr of an ident loads a single word), so
* DX (the payload) is garbage and the passthrough
* drops it. Route a tagged-ident return through the
* scratch-widen path below instead. Mirrors wwstage's
* forwardtagged kind filter, which already excludes
* N_IDENT (selfhost cgenstmt). */
int srcreg = n->lhs->kind == N_CALL ||
n->lhs->kind == N_INDEX ||
n->lhs->kind == N_DOT;
int passthrough = istagged && srcreg && (vu == rt ||
type_eq(vt, cg_ret_type));
int isstruct = vu && vu->kind == TY_STRUCT;
/* #242: a tuple variant must be PACKED into the union

View File

@@ -692,6 +692,64 @@ static const struct row rows[] = {
" case void => yield 1: i32;\n"
" };\n"
"};\n", 1, 1 },
/* #263 IDENT-source tagged return. `return v` where v is a tagged
* LOCAL ident: cstage's passthrough predicate was TYPE-only (no kind
* filter), so it forwarded the source's AX/DX unchanged — but cgexpr
* of a tagged ident loads only word0 (the tag) into AX, never the
* payload into DX, so the payload was DROPPED (cstage exited 0 on the
* i32-7 repro; wwstage exited 7 — the runtime oracle that proved
* cstage is the bug). Fix gates passthrough to the register-resident
* source kinds (N_CALL/N_INDEX/N_DOT) and routes a tagged-ident return
* through the scratch-widen path, mirroring wwstage's forwardtagged
* kind filter. byteid=1: post-fix the materialization is byte-id with
* wwstage's return scratch-widen. The void row proves the tag survives
* (the dropped-payload bug would still surface the wrong variant; the
* void tag is read correctly either way, so its survival pins the tag
* column like the #261 i32-AND-void design). */
{ "tagged_ident_ret_i32",
"package main;\n"
"fn g() (i32 | void) = { let v: (i32 | void) = 7i32; return v; };\n"
"export fn main() i32 = {\n"
" return match (g()) {\n"
" case let n: i32 => yield n;\n"
" case void => yield 1: i32;\n"
" };\n"
"};\n", 7, 1 },
{ "tagged_ident_ret_void",
"package main;\n"
"fn g() (i32 | void) = { let v: (i32 | void) = void; return v; };\n"
"export fn main() i32 = {\n"
" return match (g()) {\n"
" case let n: i32 => yield 99: i32;\n"
" case void => yield 1: i32;\n"
" };\n"
"};\n", 1, 1 },
/* #263 CONTROLS: the register-resident passthrough must STILL fire
* (don't over-gate). CALL-source (forwarding another tagged-returning
* fn) + DOT-source (a tagged struct field). INDEX-source is already
* covered by tagged_return_i32 above (`return x.o[1]`). */
{ "tagged_call_ret_ctrl",
"package main;\n"
"fn inner() (i32 | void) = { let v: (i32 | void) = 42i32; return v; };\n"
"fn outer() (i32 | void) = { return inner(); };\n"
"export fn main() i32 = {\n"
" return match (outer()) {\n"
" case let n: i32 => yield n;\n"
" case void => yield 1: i32;\n"
" };\n"
"};\n", 42, 1 },
{ "tagged_dot_ret_ctrl",
"package main;\n"
"type w = struct { f: (i32 | void) };\n"
"fn dot(x: *w) (i32 | void) = { return x.f; };\n"
"export fn main() i32 = {\n"
" let x: w;\n"
" x.f = 63;\n"
" return match (dot(&x)) {\n"
" case let n: i32 => yield n;\n"
" case void => yield 1: i32;\n"
" };\n"
"};\n", 63, 1 },
{ NULL, NULL, 0, 0 }
};