wwstage: remap error tag on try-propagate across reordered unions (#173)

wwstage cgtryprop returned the operand union's RAW tag when propagating a
`c(s)?` error, while cstage (cmd/w6c/cgen.c:6161-6184) remaps it to the
enclosing return union's variant ordering. When the operand and return
unions differ in variant order, wwstage propagated the WRONG error variant
at runtime — gate-blind: byte-id (990-997) and cstage==wwstage asm both pass
because the bootstrap only ever tries same-order unions, while the
differing-order case is silently wrong.

Port cstage's remap loop into cgtryprop (iserror-only): for each error
variant whose return-union index differs, emit the CMPQ/JNE/MOVQ/JMP that
rewrites the tag in AX; the error payload words (DX/CX/R8) are untouched and
ride the RET. Mirror cstage's emission exactly — lazy tryprop_ret allocation
on the first remap, j==i skip, j<0 fallback, no dead label when empty,
identical label strings and operand order — so same-order emits zero extra
instructions (byte-id preserved) and differing-order is now byte-identical
cstage==wwstage.

Scope: error-variant remap only. wwstage's hardcoded success-tag=0 and
iserror-only error detection (vs cstage's cg_tagged_success_tag +
cg_variant_is_error legacy fallback) diverge for non-idx-0-success or
unmarked unions — also gate-blind, also latent — filed separately as #216.

test/wcc/925_tryprop_tag_remap_run: 5 rows (differing-order for both error
variants, success unwrap, same-order byte-id witness, and a multi-word !str
payload row asserting the payload bytes survive the remap), each with a
cstage==wwstage .s byte-id check.
This commit is contained in:
2026-05-29 19:43:48 +09:00
parent f6ac7fb2f8
commit f9f83faca7
5 changed files with 493 additions and 9 deletions

View File

@@ -18377,9 +18377,8 @@ fn cgtagvariantidx(c: *cgen, tagged: *node, vt: *node) i32 = {
};
// cgtryprop — `e?` propagates the error variant up the stack.
// Legacy semantics only (success tag = 0). No tag remap; the
// selfhost code that uses ? today has the same variant order in
// operand and enclosing fn.
// Success tag = 0 (#216 tracks the legacy/flag-aware success-tag
// divergence — out of scope here, success check stays `CMPQ $0`).
fn cgtryprop(c: *cgen, n: *node) void = {
cgexpr(c, n.lhs);
// AX = tag. If non-zero, this is an error; pop frame and RET.
@@ -18388,6 +18387,56 @@ fn cgtryprop(c: *cgen, n: *node) void = {
emitline("\tJE\t");
emitline(cl);
emitline("\n");
// #173: remap the operand union's error-variant tag to the
// enclosing fn return union's variant order before propagating.
// When the `?` operand and the enclosing fn return differ in
// variant order, the raw operand tag names the WRONG variant in
// the return union. Mirrors cstage cmd/w6c/cgen.c:6161-6184.
// Payload words DX/CX/R8 ride the RET untouched; only AX (the
// tag) is rewritten. Same-order unions map every error variant to
// itself → zero instructions, byte-id with the pre-#173 emit.
let u: *tinfo = nil;
if (n.lhs != nil) { u = n.lhs.type_: *tinfo; };
for (u != nil && u.kind == tykind.TY_NAMED) { u = u.under; };
let r: *tinfo = nil;
if (c.fnret != nil) { r = c.fnret.type_: *tinfo; };
for (r != nil && r.kind == tykind.TY_NAMED) { r = r.under; };
if (u != nil && r != nil && r.kind == tykind.TY_TAGGED && u.params != nil) {
let propret: str;
propret.ptr = nil; propret.len = 0;
let haveret: bool = false;
let p: *tparam = u.params;
let i: i32 = 0;
for (p != nil) {
if (p.iserror) {
let j: i32 = flatvariantidxt(r, p.type_);
if (j < 0) { j = 0; };
if (j != i) {
let skip: str = mklabel(c, "tryprop_skip");
emitline("\tCMPQ\t$");
emitint(i: i64);
emitline(", AX\n");
emitline("\tJNE\t");
emitline(skip);
emitline("\n");
emitline("\tMOVQ\t$");
emitint(j: i64);
emitline(", AX\n");
if (!haveret) {
propret = mklabel(c, "tryprop_ret");
haveret = true;
};
emitline("\tJMP\t");
emitline(propret);
emitline("\n");
emitlabel(skip);
};
};
p = p.tnext;
i += 1;
};
if (haveret) { emitlabel(propret); };
};
emitline("\tMOVQ\tBP, SP\n\tPOPQ\tBP\n\tRET\n");
emitlabel(cl);
// Success: unwrap value. Tag-only result was AX; the rest of

View File

@@ -156,9 +156,8 @@ fn cgtagvariantidx(c: *cgen, tagged: *node, vt: *node) i32 = {
};
// cgtryprop — `e?` propagates the error variant up the stack.
// Legacy semantics only (success tag = 0). No tag remap; the
// selfhost code that uses ? today has the same variant order in
// operand and enclosing fn.
// Success tag = 0 (#216 tracks the legacy/flag-aware success-tag
// divergence — out of scope here, success check stays `CMPQ $0`).
fn cgtryprop(c: *cgen, n: *node) void = {
cgexpr(c, n.lhs);
// AX = tag. If non-zero, this is an error; pop frame and RET.
@@ -167,6 +166,56 @@ fn cgtryprop(c: *cgen, n: *node) void = {
emitline("\tJE\t");
emitline(cl);
emitline("\n");
// #173: remap the operand union's error-variant tag to the
// enclosing fn return union's variant order before propagating.
// When the `?` operand and the enclosing fn return differ in
// variant order, the raw operand tag names the WRONG variant in
// the return union. Mirrors cstage cmd/w6c/cgen.c:6161-6184.
// Payload words DX/CX/R8 ride the RET untouched; only AX (the
// tag) is rewritten. Same-order unions map every error variant to
// itself → zero instructions, byte-id with the pre-#173 emit.
let u: *tinfo = nil;
if (n.lhs != nil) { u = n.lhs.type_: *tinfo; };
for (u != nil && u.kind == tykind.TY_NAMED) { u = u.under; };
let r: *tinfo = nil;
if (c.fnret != nil) { r = c.fnret.type_: *tinfo; };
for (r != nil && r.kind == tykind.TY_NAMED) { r = r.under; };
if (u != nil && r != nil && r.kind == tykind.TY_TAGGED && u.params != nil) {
let propret: str;
propret.ptr = nil; propret.len = 0;
let haveret: bool = false;
let p: *tparam = u.params;
let i: i32 = 0;
for (p != nil) {
if (p.iserror) {
let j: i32 = flatvariantidxt(r, p.type_);
if (j < 0) { j = 0; };
if (j != i) {
let skip: str = mklabel(c, "tryprop_skip");
emitline("\tCMPQ\t$");
emitint(i: i64);
emitline(", AX\n");
emitline("\tJNE\t");
emitline(skip);
emitline("\n");
emitline("\tMOVQ\t$");
emitint(j: i64);
emitline(", AX\n");
if (!haveret) {
propret = mklabel(c, "tryprop_ret");
haveret = true;
};
emitline("\tJMP\t");
emitline(propret);
emitline("\n");
emitlabel(skip);
};
};
p = p.tnext;
i += 1;
};
if (haveret) { emitlabel(propret); };
};
emitline("\tMOVQ\tBP, SP\n\tPOPQ\tBP\n\tRET\n");
emitlabel(cl);
// Success: unwrap value. Tag-only result was AX; the rest of

View File

@@ -18377,9 +18377,8 @@ fn cgtagvariantidx(c: *cgen, tagged: *node, vt: *node) i32 = {
};
// cgtryprop — `e?` propagates the error variant up the stack.
// Legacy semantics only (success tag = 0). No tag remap; the
// selfhost code that uses ? today has the same variant order in
// operand and enclosing fn.
// Success tag = 0 (#216 tracks the legacy/flag-aware success-tag
// divergence — out of scope here, success check stays `CMPQ $0`).
fn cgtryprop(c: *cgen, n: *node) void = {
cgexpr(c, n.lhs);
// AX = tag. If non-zero, this is an error; pop frame and RET.
@@ -18388,6 +18387,56 @@ fn cgtryprop(c: *cgen, n: *node) void = {
emitline("\tJE\t");
emitline(cl);
emitline("\n");
// #173: remap the operand union's error-variant tag to the
// enclosing fn return union's variant order before propagating.
// When the `?` operand and the enclosing fn return differ in
// variant order, the raw operand tag names the WRONG variant in
// the return union. Mirrors cstage cmd/w6c/cgen.c:6161-6184.
// Payload words DX/CX/R8 ride the RET untouched; only AX (the
// tag) is rewritten. Same-order unions map every error variant to
// itself → zero instructions, byte-id with the pre-#173 emit.
let u: *tinfo = nil;
if (n.lhs != nil) { u = n.lhs.type_: *tinfo; };
for (u != nil && u.kind == tykind.TY_NAMED) { u = u.under; };
let r: *tinfo = nil;
if (c.fnret != nil) { r = c.fnret.type_: *tinfo; };
for (r != nil && r.kind == tykind.TY_NAMED) { r = r.under; };
if (u != nil && r != nil && r.kind == tykind.TY_TAGGED && u.params != nil) {
let propret: str;
propret.ptr = nil; propret.len = 0;
let haveret: bool = false;
let p: *tparam = u.params;
let i: i32 = 0;
for (p != nil) {
if (p.iserror) {
let j: i32 = flatvariantidxt(r, p.type_);
if (j < 0) { j = 0; };
if (j != i) {
let skip: str = mklabel(c, "tryprop_skip");
emitline("\tCMPQ\t$");
emitint(i: i64);
emitline(", AX\n");
emitline("\tJNE\t");
emitline(skip);
emitline("\n");
emitline("\tMOVQ\t$");
emitint(j: i64);
emitline(", AX\n");
if (!haveret) {
propret = mklabel(c, "tryprop_ret");
haveret = true;
};
emitline("\tJMP\t");
emitline(propret);
emitline("\n");
emitlabel(skip);
};
};
p = p.tnext;
i += 1;
};
if (haveret) { emitlabel(propret); };
};
emitline("\tMOVQ\tBP, SP\n\tPOPQ\tBP\n\tRET\n");
emitlabel(cl);
// Success: unwrap value. Tag-only result was AX; the rest of