From dc33af6217a57a2275bb6a05c65d302507223958 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sat, 8 Aug 2026 17:11:50 +0900 Subject: [PATCH] w6c: remap legacy-union error tags in wwstage `?` propagation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The #173 remap loop in cgtryprop gated on the tparam's explicit `!` flag only; a LEGACY union (no marks anywhere) classifies error variants POSITIONALLY (index 0 is success, cstage cg_variant_is_error cgen.c:905) — so wwstage emitted no remap and propagated the callee's raw tag into a differently-ordered caller union, a silent wrong arm (cstage exit 7, wwstage 9 on the banked r700 row). Add the variantiserror twin and gate the loop on it; refresh the drifted cstage cite. Graduates the held e2e row into the corpus (pin 1727/1156/3454) and adds legacy-mode rows to test/lang/tryprop_tag_remap_test.ww: reversed order both directions, slot-0 remap, str payload integrity, a two-hop chain whose pre-fix runtime pass was double-miss tag cancellation (the lang byteid leg pins the emitted remap blocks), and a same-order zero-emission control. --- internal/wwfixture/types.ww | 8 +- selfhost/cmd/wcc/cgenexpr.ww | 32 ++++++- test/lang/tryprop_tag_remap_test.ww | 112 ++++++++++++++++++++++++ test/wcc/data/r700_try_rev_errs/case.ww | 21 +++++ 4 files changed, 167 insertions(+), 6 deletions(-) create mode 100644 test/wcc/data/r700_try_rev_errs/case.ww diff --git a/internal/wwfixture/types.ww b/internal/wwfixture/types.ww index 6f6c2d15..e5f76923 100644 --- a/internal/wwfixture/types.ww +++ b/internal/wwfixture/types.ww @@ -1,13 +1,13 @@ package wwfixture; def protocolversion: i32 = 1; -def corpuscount: i32 = 1726; +def corpuscount: i32 = 1727; def errorcount: i32 = 343; def compilecount: i32 = 19; def runcount: i32 = 209; -def runexitcount: i32 = 1155; -def nativecount: i32 = 3452; -def corpushash: str = "870ba4f5140ef331e9effb7cf21fcebe69b5f29d1177cf158e6a1b6db71e9d28"; +def runexitcount: i32 = 1156; +def nativecount: i32 = 3454; +def corpushash: str = "40ad5fd79515a743f2adb53e4a26396c6f13859df956fa079299a4b459594177"; type directive = enum i32 { ERROR = 0, diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 5cd65ec4..7c5ee504 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -200,6 +200,34 @@ fn successvariant(ou: *syntax.tinfo) *syntax.tinfo = { return nil; }; +// variantiserror — is variant idx of tagged union u an error variant? +// Mirrors cstage cg_variant_is_error (cmd/w6c/cgen.c:905): explicit +// `!`-marked unions use the per-variant flag; a LEGACY union (no `!` +// marks anywhere) treats index 0 as success and every other variant as +// an error. The #173 remap loop must use THIS predicate, not the bare +// flag — the flag-only gate skipped legacy unions entirely, so `?` +// propagated the callee's raw tag into a reordered caller union +// (silent wrong arm). +fn variantiserror(ou: *syntax.tinfo, idx: i32) bool = { + let u: *syntax.tinfo = tichase(ou); + if (u == nil) { return false; }; + if (u.kind != syntax.tykind.TY_TAGGED) { return false; }; + let haserr: bool = false; + let p: *syntax.tparam = u.params; + for (p != nil) { if (p.iserror) { haserr = true; }; p = p.tnext; }; + let i: i32 = 0; + p = u.params; + for (p != nil) { + if (i == idx) { + if (haserr) { return p.iserror; }; + return idx != 0; + }; + i += 1; + p = p.tnext; + }; + return false; +}; + // 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 @@ -502,7 +530,7 @@ fn cgtryprop(c: *cgen, n: *syntax.node) void = { // 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. + // the return union. Mirrors cstage cmd/w6c/cgen.c:11296-11319. // 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. @@ -519,7 +547,7 @@ fn cgtryprop(c: *cgen, n: *syntax.node) void = { let p: *syntax.tparam = u.params; let i: i32 = 0; for (p != nil) { - if (p.iserror) { + if (variantiserror(u, i)) { let j: i32 = flatvariantidxt(r, p.type_, false); if (j < 0) { j = 0; }; if (j != i) { diff --git a/test/lang/tryprop_tag_remap_test.ww b/test/lang/tryprop_tag_remap_test.ww index ed6dee7e..76a1f437 100644 --- a/test/lang/tryprop_tag_remap_test.ww +++ b/test/lang/tryprop_tag_remap_test.ww @@ -80,3 +80,115 @@ fn f_mw(which: i32) (i32 | emsg | e1) = { case e1 => { assert(false); }; }; }; + +// Legacy (unmarked) unions: no variant carries `!`, so index 0 is the +// success and every other variant is an error POSITIONALLY (cstage +// cg_variant_is_error legacy arm). The remap must fire from that +// classification, not the `!` flag — the flag-only gate skipped legacy +// unions entirely and `?` propagated the callee's raw tag into a +// reordered caller union (the r700_try_rev_errs silent miscompile). + +fn lg(which: i32) (i32 | str | bool) = { + if (which == 1) { return "z"; }; + if (which == 2) { return false; }; + return 100; +}; + +// caller error order (bool, str) reverses lg's (str, bool): 1->2, 2->1. +fn lf_rev(which: i32) (i64 | bool | str) = { + let v: i32 = lg(which)?; + return v: i64 + 1; +}; + +@test fn legacy_rev_str() void = { + match (lf_rev(1)) { + case let v: i64 => { assert(false); }; + case bool => { assert(false); }; + case let s: str => { assert(s.len == 1); assert(s[0] == 122u8); }; + }; +}; + +@test fn legacy_rev_bool() void = { + match (lf_rev(2)) { + case let v: i64 => { assert(false); }; + case let b: bool => { assert(!b); }; + case str => { assert(false); }; + }; +}; + +@test fn legacy_rev_success() void = { + match (lf_rev(0)) { + case let v: i64 => { assert(v == 101i64); }; + case bool => { assert(false); }; + case str => { assert(false); }; + }; +}; + +// the propagated error lands at slot 0 of the enclosing union. +fn lg0(which: i32) (i64 | str | bool) = { + if (which == 1) { return false; }; + if (which == 2) { return "q"; }; + return 5i64; +}; +fn lf_slot0(which: i32) (bool | i64 | str) = { + let v: i64 = lg0(which)?; + return v + 1; +}; + +@test fn legacy_slot0() void = { + match (lf_slot0(1)) { + case let b: bool => { assert(!b); }; + case i64 => { assert(false); }; + case str => { assert(false); }; + }; + match (lf_slot0(0)) { + case bool => { assert(false); }; + case let v: i64 => { assert(v == 6i64); }; + case str => { assert(false); }; + }; +}; + +// two chained remaps: bool rides 2->1->2 across differently-ordered +// unions. Runtime alone can pass by double-miss cancellation (2->2->2); +// the lang byte-id leg pins the emitted remap blocks in BOTH hops. +fn n2(w: i32) (i64 | bool | str) = { + let v: i32 = lg(w)?; + return v: i64; +}; +fn n3(w: i32) (u8 | str | bool) = { + let v: i64 = n2(w)?; + return v: u8; +}; + +@test fn legacy_nested_two_hop() void = { + match (n3(2)) { + case u8 => { assert(false); }; + case str => { assert(false); }; + case let b: bool => { assert(!b); }; + }; + match (n3(0)) { + case let v: u8 => { assert(v == 100u8); }; + case str => { assert(false); }; + case bool => { assert(false); }; + }; +}; + +// same error order end to end: the remap loop maps every variant to +// itself and must emit NOTHING (byte-id with the pre-remap window). +fn sctl(w: i32) (i64 | str | bool) = { + let v: i32 = lg(w)?; + return v: i64; +}; + +@test fn legacy_same_order() void = { + match (sctl(2)) { + case i64 => { assert(false); }; + case str => { assert(false); }; + case let b: bool => { assert(!b); }; + }; + match (sctl(0)) { + case let v: i64 => { assert(v == 100i64); }; + case str => { assert(false); }; + case bool => { assert(false); }; + }; +}; diff --git a/test/wcc/data/r700_try_rev_errs/case.ww b/test/wcc/data/r700_try_rev_errs/case.ww new file mode 100644 index 00000000..8abe0dcc --- /dev/null +++ b/test/wcc/data/r700_try_rev_errs/case.ww @@ -0,0 +1,21 @@ +//ww:run-exit 7 +// Migrated from 700_e2e row 69. +package main; +fn inner(n: i32) (i32 | str | bool) = { + if (n == 0) { return "z"; }; + if (n < 0) { return false; }; + return n; +}; +fn outer(n: i32) (i64 | bool | str) = { + let v: i32 = inner(n)?; + return v: i64 + 1000; +}; +fn main() i32 = { + let r: (i64 | bool | str) = outer(-1); + match (r) { + case let n: i64 => return n: i32; + case let b: bool => { if (!b) { return 7; }; return 8; }; + case let s: str => return 9; + }; + return 0; +};