From 0afe4225cd2234acc0c051b121bd137a0c81cf89 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 2 Jun 2026 06:28:00 +0900 Subject: [PATCH] =?UTF-8?q?w6c:=20materialize=20full=20tagged=20slot=20on?= =?UTF-8?q?=20N=5FIDENT-source=20return=20(#263)=20=E2=80=94=20cstage=20al?= =?UTF-8?q?ign-up=20to=20wwstage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- cmd/w6c/cgen.c | 16 +++++++- test/wcc/949_dotbase_addr_slice_run.c | 58 +++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index d227dcc7..15b486d4 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -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 diff --git a/test/wcc/949_dotbase_addr_slice_run.c b/test/wcc/949_dotbase_addr_slice_run.c index 726f468a..57442e14 100644 --- a/test/wcc/949_dotbase_addr_slice_run.c +++ b/test/wcc/949_dotbase_addr_slice_run.c @@ -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 } };