diff --git a/Makefile b/Makefile index d4325840..9a1aeb28 100644 --- a/Makefile +++ b/Makefile @@ -253,6 +253,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_struct_global_byval_arg \ $(BIN)/test_inferred_global_let \ $(BIN)/test_inferred_float_global \ + $(BIN)/test_errfirst_union_try \ $(BIN)/test_slice_str_global_zero \ $(BIN)/test_slice_literal_global \ $(BIN)/test_global_arr_elem_field \ @@ -679,6 +680,12 @@ $(BIN)/test_inferred_float_global: test/wcc/824_inferred_float_global.c $(BIN)/w $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_errfirst_union_try: test/wcc/825_errfirst_union_try.c $(BIN)/ww \ + $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_def_arr_infer_len: test/wcc/814_def_arr_infer_len.c $(BIN)/ww \ $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 66a506fe..befaa6aa 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -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"); diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 5c158097..d946a7f5 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -162,12 +162,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; @@ -175,7 +217,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; }; @@ -184,7 +226,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 @@ -198,7 +240,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; }; @@ -281,8 +323,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 @@ -307,9 +349,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"); @@ -367,7 +416,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 @@ -422,8 +471,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) { @@ -438,7 +488,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"); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 28561603..416c0f56 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -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"); diff --git a/test/wcc/825_errfirst_union_try.c b/test/wcc/825_errfirst_union_try.c new file mode 100644 index 00000000..582313ae --- /dev/null +++ b/test/wcc/825_errfirst_union_try.c @@ -0,0 +1,279 @@ +/* + * 825_errfirst_union_try — error-first tagged-union try (`?`/`!`) success-tag + * (#52, the #216 deferred divergence). wwstage HARDCODED the success tag as 0, + * so for an ERROR-FIRST union — error variant at tag 0, success at tag 1+ (e.g. + * `(e | u16)` with `type e = !i32`) — the success (tag 1) failed the `CMPQ $0` + * success check and fell to the error path: `!` → exit(1), `?` → propagate. + * cstage was already correct via cg_tagged_success_tag (cmd/w6c/cgen.c:857): + * if any variant is an error, the success tag is the first NON-error variant's + * index, else 0. The `.s` diff was literally `CMPQ $1` (cstage) vs `CMPQ $0` + * (wwstage). + * + * The fix (selfhost cgenexpr.ww only — cstage UNCHANGED): a `successtag` + * helper mirroring cstage's, applied at the FOUR sites that shared the tag-0 / + * first-param assumption — cgtryprop CMPQ, cgtryunw CMPQ, and the + * cgtrytupleshift / cgtrytaggedshift payload-variant lookups (successvariant). + * + * row | shape | want + * ----------------+----------------------------------------+------ + * errfirst_must | (e|u16) `!` unwrap (THE repro) | 44 + * errfirst_prop | (e|u16) `?` propagate through outer fn | 44 + * succfirst_ctrl | (u16|e) `!` — successtag=0, CMPQ $0 | 7 + * errfirst_3var | (e1|e2|u16) success at index 2 | 99 + * errfirst_tuple | (e|(u16,u16)) `!` — successvariant shift| 51 + * + * errfirst_tuple is the LATENT-site row: the cgtrytupleshift payload-shift only + * bites an error-first union whose SUCCESS variant is itself a tuple. Pre-fix it + * fell two ways — the CMPQ $0 rejected tag-1 (→ exit 1), AND even with the CMPQ + * fixed the shift read ou.params.type_ (the error variant, not a tuple) so the + * rvalue tuple never filled the cursor (verified mutation: 88, not 51). The + * successvariant fix routes the shift to the tuple. The sibling cgtrytaggedshift + * (nested-tagged success) shares the identical successvariant call; it is not + * exercised here because nested-tagged union *construction* (the return-coercion + * box, main.ok) has a separate pre-existing cs!=ww divergence — #52-independent + * (present success-first too) — filed for its own fix. + * + * Exit codes are the process low-8-bits: 300 & 0xFF = 44. errfirst_* are the + * mutation-sane rows: pre-fix wwstage = exit 1 (wrong-tag → error path), + * cstage = correct → cs!=ww. succfirst_ctrl is the byte-id no-regress guard: + * successtag returns 0 there, so its `CMPQ $0` is UNCHANGED. A byte-id pass + * pins cstage==wwstage `.s` for every row. + */ +#include +#include +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return -1; +} + +struct row { const char *label; const char *src; int want; }; + +static const struct row rows[] = { + /* errfirst_must — THE repro: error-first `(e|u16)`, `!` unwrap of a + * tag-1 success. Pre-fix wwstage CMPQ $0 vs tag 1 → error → exit 1. */ + { "errfirst_must", + "package main;\n" + "type e = !i32;\n" + "type u = (e | u16);\n" + "fn ok() u = { return 300u16; };\n" + "export fn main() i32 = {\n" + "\tlet x = ok()!;\n" + "\treturn x: i32;\n" + "};\n", + 44 }, + + /* errfirst_prop — the `?` twin: outer propagates inner's success + * through the same error-first union, then `!` unwraps. */ + { "errfirst_prop", + "package main;\n" + "type e = !i32;\n" + "type u = (e | u16);\n" + "fn inner() u = { return 300u16; };\n" + "fn outer() u = {\n" + "\tlet v = inner()?;\n" + "\treturn v;\n" + "};\n" + "export fn main() i32 = {\n" + "\treturn outer()!: i32;\n" + "};\n", + 44 }, + + /* succfirst_ctrl — success-FIRST `(u16|e)`, successtag=0: the byte-id + * no-regress guard, CMPQ $0 unchanged. */ + { "succfirst_ctrl", + "package main;\n" + "type e = !i32;\n" + "type u2 = (u16 | e);\n" + "fn g() u2 = { return 7u16; };\n" + "export fn main() i32 = {\n" + "\treturn g()!: i32;\n" + "};\n", + 7 }, + + /* errfirst_3var — two error variants then success: successtag walks + * past index 0 AND 1 to return 2, pinning the walk (not 0, not 1). */ + { "errfirst_3var", + "package main;\n" + "type e1 = !i32;\n" + "type e2 = !i64;\n" + "type u3 = (e1 | e2 | u16);\n" + "fn h() u3 = { return 99u16; };\n" + "export fn main() i32 = {\n" + "\treturn h()!: i32;\n" + "};\n", + 99 }, + + /* errfirst_tuple — error-first union whose SUCCESS variant is a tuple: + * exercises cgtrytupleshift via successvariant. Pre-fix exit 1 (CMPQ $0 + * rejects tag 1); CMPQ-fixed-but-shift-reverted gives 88; full fix → 51 + * (300+7 = 307, 307 & 0xFF = 51). */ + { "errfirst_tuple", + "package main;\n" + "type e = !i32;\n" + "type ut = (e | (u16, u16));\n" + "fn ok() ut = { return (300u16, 7u16); };\n" + "export fn main() i32 = {\n" + "\tlet t = ok()!;\n" + "\tlet (a, b) = t;\n" + "\treturn (a: i32) + (b: i32);\n" + "};\n", + 51 }, +}; + +static int +run_driver(const char *driver, const struct row *r, int i) +{ + char src[64], tmpdir[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/efu_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/efu_%d_d_%d", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>/dev/null", + tmpdir, driver, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: build via %s failed\n", + r->label, driver); + unlink(src); rmdir(tmpdir); + return -1; + } + + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + char outbin[128]; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + int got = runwait(outbin); + + unlink(src); unlink(outbin); rmdir(tmpdir); + return got; +} + +/* asm_byte_identical — w6c vs w6c_ww .s for the same source must match. */ +static int +asm_byte_identical(const char *bin, const struct row *r, int i) +{ + char src[64], cs[64], ws[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/efu_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/efu_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/efu_asm_%d_%d_w.s", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s 2>/dev/null", bin, cs, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c errored\n", r->label); + unlink(src); + return -1; + } + snprintf(cmd, sizeof cmd, "%s/w6c_ww -o %s %s 2>/dev/null", + bin, ws, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c_ww errored\n", r->label); + unlink(src); unlink(cs); + return -1; + } + + FILE *fc = fopen(cs, "rb"); + FILE *fw = fopen(ws, "rb"); + int rc = 0; + if (!fc || !fw) { + rc = -1; + } else { + for (;;) { + int a = fgetc(fc); + int b = fgetc(fw); + if (a != b) { rc = -1; break; } + if (a == EOF) break; + } + } + if (fc) fclose(fc); + if (fw) fclose(fw); + if (rc != 0) + fprintf(stderr, "row[%s]: cstage vs wwstage asm differs\n", + r->label); + unlink(src); unlink(cs); unlink(ws); + return rc; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char cdrv[1024]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + char wdrv[1024]; + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + struct { const char *name; const char *path; int gated_on_existence; } + drivers[] = { + { "cstage", cdrv, 0 }, + { "wwstage", wdrv, 1 }, + { NULL, NULL, 0 }, + }; + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + + for (int d = 0; drivers[d].name; d++) { + if (drivers[d].gated_on_existence + && access(drivers[d].path, X_OK) != 0) { + fprintf(stderr, "errfirst_union_try: skip %s (no %s)\n", + drivers[d].name, drivers[d].path); + continue; + } + for (int i = 0; i < n; i++) { + int got = run_driver(drivers[d].path, &rows[i], i); + total++; + if (got != rows[i].want) { + fprintf(stderr, + "errfirst_union_try[%s][%s]: exit=%d want=%d\n", + drivers[d].name, rows[i].label, + got, rows[i].want); + fail++; + } + } + } + + if (access(wdrv, X_OK) == 0) { + for (int i = 0; i < n; i++) { + total++; + if (asm_byte_identical(bin, &rows[i], i) != 0) + fail++; + } + } + + if (fail) { + fprintf(stderr, + "errfirst_union_try: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("errfirst_union_try: %d/%d ok\n", total, total); + return 0; +}