wcc/cgen: #52 error-first tagged-union success tag — successtag helper not hardcoded 0 (wwstage)

An error-first tagged union -- error variant at tag 0, success at tag 1+,
e.g. (myerr | u16) -- was silently miscompiled by wwstage: the try/propagate
codegen hardcoded success = tag 0, so the actual success value (tag 1)
failed the CMPQ $0 and fell to the error path -> exit(1) instead of the
value (44). cstage was correct (computes the success tag via
cg_tagged_success_tag = first non-error variant).

wwstage-only: a successtag/successvariant helper (mirroring cstage) replaces
the hardcoded tag-0 / first-param assumption at all four try sites --
cgtryprop (?), cgtryunw (!), and the two latent shift sites cgtrytupleshift
+ cgtrytaggedshift (which bite an error-first union with an aggregate
success payload). Success-first unions (the Hare idiom + what the selfhost
uses) keep successtag=0 -> CMPQ $0 unchanged -> byte-id-neutral on 990-997.
cstage untouched (w6c md5 unchanged).

byte-id 990-997 8/8. test/wcc/825 table-driven (errfirst must/prop +
tuple-success + success-first control). A separate nested-tagged-union
construction divergence is filed (#10/#125).
This commit is contained in:
2026-06-08 22:15:19 +09:00
parent ef6fcbfc04
commit fca979470f
5 changed files with 502 additions and 45 deletions

View File

@@ -21745,12 +21745,54 @@ fn cgtagvariantidx(c: *cgen, tagged: *node, vt: *node) i32 = {
return flatvariantidx(c, tagged, vt);
};
// cgtrytupleshift — #241: if the `?`/`!` operand's success variant (tag 0)
// is a tuple, the unwrapped payload is an rvalue tuple that must fill the
// successtag — index of the success variant of a tagged union. Mirrors
// cstage cg_tagged_success_tag (cmd/w6c/cgen.c:857): if any variant is an
// error (`!T`), the success tag is the first NON-error variant's index;
// else 0 (legacy/no-error). Drives the `?`/`!` success-tag CMPQ + the
// payload-shift variant lookup (#52/#216 — replaces the hardcoded tag-0).
fn successtag(ou: *tinfo) i64 = {
let u: *tinfo = tichase(ou);
if (u == nil) { return 0i64; };
if (u.kind != tykind.TY_TAGGED) { return 0i64; };
let haserr: bool = false;
let p: *tparam = u.params;
for (p != nil) { if (p.iserror) { haserr = true; }; p = p.tnext; };
if (!haserr) { return 0i64; };
let idx: i64 = 0i64;
p = u.params;
for (p != nil) {
if (!p.iserror) { return idx; };
idx += 1i64;
p = p.tnext;
};
return 0i64;
};
// successvariant — the success variant's type_ (the param at successtag).
// Lets the #241 tuple/tagged payload-shift sites inspect the SUCCESS
// variant's shape, not the (error-first) first param.
fn successvariant(ou: *tinfo) *tinfo = {
let u: *tinfo = tichase(ou);
if (u == nil) { return nil; };
if (u.kind != tykind.TY_TAGGED) { return nil; };
let st: i64 = successtag(ou);
let idx: i64 = 0i64;
let p: *tparam = u.params;
for (p != nil) {
if (idx == st) { return p.type_; };
idx += 1i64;
p = p.tnext;
};
return nil;
};
// cgtrytupleshift — #241: if the `?`/`!` operand's success variant is a
// tuple, the unwrapped payload is an rvalue tuple that must fill the
// register cursor (shift past the tag), and the scalar/str MOVQ DX,AX tail
// is skipped. Returns true when it emitted the shift. Reads the operand's
// stamped tagged result tinfo (n.lhs.type_) the success variant is the
// first param, matching the `CMPQ $0` success-tag convention.
// stamped tagged result tinfo (n.lhs.type_); the success variant is the
// param at successtag (dynamic, #52 — not the hardcoded first param),
// matching the dynamic CMPQ $successtag success-tag convention.
fn cgtrytupleshift(c: *cgen, n: *node) bool = {
if (n.lhs == nil) { return false; };
let ou: *tinfo = n.lhs.type_: *tinfo;
@@ -21758,7 +21800,7 @@ fn cgtrytupleshift(c: *cgen, n: *node) bool = {
if (ou == nil) { return false; };
if (ou.kind != tykind.TY_TAGGED) { return false; };
if (ou.params == nil) { return false; };
let sv: *tinfo = ou.params.type_;
let sv: *tinfo = successvariant(ou);
sv = tichase(sv);
if (sv == nil) { return false; };
if (sv.kind != tykind.TY_TUPLE) { return false; };
@@ -21767,7 +21809,7 @@ fn cgtrytupleshift(c: *cgen, n: *node) bool = {
};
// cgtrytaggedshift — Family C (#35, unwrap source): if the `?`/`!`
// operand's success variant (tag 0) is itself a TAGGED union, the
// operand's success variant (at successtag, #52) is itself a TAGGED union, the
// unwrapped value is a NESTED box (ww keeps nested unions
// un-flattened) riding the payload words intact — shift past the
// outer tag so consumers see the standard AX=tag cursor. The scalar
@@ -21781,7 +21823,7 @@ fn cgtrytaggedshift(c: *cgen, n: *node) bool = {
if (ou == nil) { return false; };
if (ou.kind != tykind.TY_TAGGED) { return false; };
if (ou.params == nil) { return false; };
let sv: *tinfo = ou.params.type_;
let sv: *tinfo = successvariant(ou);
sv = tichase(sv);
if (sv == nil) { return false; };
if (sv.kind != tykind.TY_TAGGED) { return false; };
@@ -21864,8 +21906,8 @@ fn cgtryunwcursor(c: *cgen, n: *node, opname: str) void = {
};
// cgtryprop — `e?` propagates the error variant up the stack.
// Success tag = 0 (#216 tracks the legacy/flag-aware success-tag
// divergence — out of scope here, success check stays `CMPQ $0`).
// Success tag is dynamic via successtag (#52/#216): the first non-error
// variant's index (cstage cg_tagged_success_tag parity), 0 when success-first.
fn cgtryprop(c: *cgen, n: *node) void = {
// #38b residuals (rule 7): the cursor read below cannot see an
// sret-classified call result (AX = dest pointer), and the
@@ -21890,9 +21932,16 @@ fn cgtryprop(c: *cgen, n: *node) void = {
// cursor here (was a silent word0 unwrap); call sources keep
// the plain cgexpr emission byte-for-byte.
cgtryunwcursor(c, n, "?");
// AX = tag. If non-zero, this is an error; pop frame and RET.
// AX = tag. If not the success tag, this is an error; pop frame and
// RET. Success tag is dynamic (successtag / cstage cg_tagged_success_tag
// parity, #52): 0 for success-first, the first non-error index for an
// error-first union.
let cl: str = mklabel(c, "tryprop_ok");
emitline("\tCMPQ\t$0, AX\n");
let propu: *tinfo = nil;
if (n.lhs != nil) { propu = n.lhs.type_: *tinfo; };
emitline("\tCMPQ\t$");
emitint(successtag(propu));
emitline(", AX\n");
emitline("\tJE\t");
emitline(cl);
emitline("\n");
@@ -21950,7 +21999,7 @@ fn cgtryprop(c: *cgen, n: *node) void = {
emitlabel(cl);
// #241: a tuple success payload is an rvalue tuple — fill the cursor
// (shift past the tag) so the destructure / let consumer reads every
// element, not just word0. Success variant = tag 0 (first param).
// element, not just word0. Success variant = successtag (#52).
if (cgtrytupleshift(c, n)) { return; };
if (cgtrytaggedshift(c, n)) { return; };
// Success: unwrap value. Tag-only result was AX; the rest of
@@ -22005,8 +22054,9 @@ fn cgtryprop(c: *cgen, n: *node) void = {
return;
};
// cgtryunw — `e!` aborts on the error variant via exit(1). Legacy
// semantics (success tag = 0).
// cgtryunw — `e!` aborts on the error variant via exit(1). Success tag
// is dynamic via successtag (#52): the first non-error variant's index
// (cstage cg_tagged_success_tag parity), 0 when success-first.
fn cgtryunw(c: *cgen, n: *node) void = {
// #38b residual (rule 7): see the cgtryprop twin.
if (n.lhs != nil) {
@@ -22021,7 +22071,14 @@ fn cgtryunw(c: *cgen, n: *node) void = {
// Family C (#35/#46): see the cgtryprop twin.
cgtryunwcursor(c, n, "!");
let cl: str = mklabel(c, "tryunw_ok");
emitline("\tCMPQ\t$0, AX\n");
// Success tag is dynamic (successtag / cstage cg_tagged_success_tag
// parity, #52): 0 for success-first, the first non-error index for an
// error-first union.
let unwu: *tinfo = nil;
if (n.lhs != nil) { unwu = n.lhs.type_: *tinfo; };
emitline("\tCMPQ\t$");
emitint(successtag(unwu));
emitline(", AX\n");
emitline("\tJE\t");
emitline(cl);
emitline("\n");