diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 89977179..4bea0549 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -11174,9 +11174,40 @@ fn typeeqast(a: *node, b: *node) bool = { }; return pb == nil; }; - // Conservative: anything else (struct/tagged/array) fails the - // cheap check. Selfhost code doesn't currently rely on equality - // at these shapes for the targeted checks. + // #47 gap-B: tagged structural equality — mirror of cstage + // type.c:288-300 (TY_TAGGED). A tuple member with a TAGGED element + // (e.g. ((void|size),(void|size),size)) recurses here from the + // N_TTUPLE arm; without it the per-element compare falls to the + // catch-all and the whole case-against-tagged-scrutinee is rejected. + // Variants are DIRECT .list nodes (casevariantin walks tagged.list + // + typeeqast(v,..) directly), NOT N_TPARAM-wrapped like tuple elems. + // Divergence: cstage's nullable-flag check (type.c:293) is a resolved- + // Type property with no AST analogue; for case-match both sides share + // a spelling so it's moot — no phantom AST nullable check. + if (k == nkind.N_TTAGGED) { + let pa: *node = aa.list; + let pb: *node = bb.list; + for (pa != nil) { + if (pb == nil) { return false; }; + // #115: a `...inner` spread variant stays unflattened at + // this AST layer (casevariantin flattens only at the OUTER + // level; cstage flattens in resolve_type before type_eq ever + // runs). A position-by-position compare cannot honour it — + // the N_TNAME leg is pure streq, so `...ab` would silently + // match a plain `ab`, accepting a case cstage rejects. + // Conservatively loud-reject any spread until the flatten + // lands; b1c's (void|size) has none, so byte-id is untouched. + if (pa.op == tkind.TK_ELLIPSIS) { return false; }; + if (pb.op == tkind.TK_ELLIPSIS) { return false; }; + if (!typeeqast(pa, pb)) { return false; }; + pa = pa.next; + pb = pb.next; + }; + return pb == nil; + }; + // Conservative: anything else (struct/array) fails the cheap + // check. Selfhost code doesn't currently rely on equality at + // these shapes for the targeted checks. return false; }; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 9659560d..cfc8bfcb 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -893,9 +893,40 @@ fn typeeqast(a: *node, b: *node) bool = { }; return pb == nil; }; - // Conservative: anything else (struct/tagged/array) fails the - // cheap check. Selfhost code doesn't currently rely on equality - // at these shapes for the targeted checks. + // #47 gap-B: tagged structural equality — mirror of cstage + // type.c:288-300 (TY_TAGGED). A tuple member with a TAGGED element + // (e.g. ((void|size),(void|size),size)) recurses here from the + // N_TTUPLE arm; without it the per-element compare falls to the + // catch-all and the whole case-against-tagged-scrutinee is rejected. + // Variants are DIRECT .list nodes (casevariantin walks tagged.list + // + typeeqast(v,..) directly), NOT N_TPARAM-wrapped like tuple elems. + // Divergence: cstage's nullable-flag check (type.c:293) is a resolved- + // Type property with no AST analogue; for case-match both sides share + // a spelling so it's moot — no phantom AST nullable check. + if (k == nkind.N_TTAGGED) { + let pa: *node = aa.list; + let pb: *node = bb.list; + for (pa != nil) { + if (pb == nil) { return false; }; + // #115: a `...inner` spread variant stays unflattened at + // this AST layer (casevariantin flattens only at the OUTER + // level; cstage flattens in resolve_type before type_eq ever + // runs). A position-by-position compare cannot honour it — + // the N_TNAME leg is pure streq, so `...ab` would silently + // match a plain `ab`, accepting a case cstage rejects. + // Conservatively loud-reject any spread until the flatten + // lands; b1c's (void|size) has none, so byte-id is untouched. + if (pa.op == tkind.TK_ELLIPSIS) { return false; }; + if (pb.op == tkind.TK_ELLIPSIS) { return false; }; + if (!typeeqast(pa, pb)) { return false; }; + pa = pa.next; + pb = pb.next; + }; + return pb == nil; + }; + // Conservative: anything else (struct/array) fails the cheap + // check. Selfhost code doesn't currently rely on equality at + // these shapes for the targeted checks. return false; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 121a77de..41f5640c 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -11174,9 +11174,40 @@ fn typeeqast(a: *node, b: *node) bool = { }; return pb == nil; }; - // Conservative: anything else (struct/tagged/array) fails the - // cheap check. Selfhost code doesn't currently rely on equality - // at these shapes for the targeted checks. + // #47 gap-B: tagged structural equality — mirror of cstage + // type.c:288-300 (TY_TAGGED). A tuple member with a TAGGED element + // (e.g. ((void|size),(void|size),size)) recurses here from the + // N_TTUPLE arm; without it the per-element compare falls to the + // catch-all and the whole case-against-tagged-scrutinee is rejected. + // Variants are DIRECT .list nodes (casevariantin walks tagged.list + // + typeeqast(v,..) directly), NOT N_TPARAM-wrapped like tuple elems. + // Divergence: cstage's nullable-flag check (type.c:293) is a resolved- + // Type property with no AST analogue; for case-match both sides share + // a spelling so it's moot — no phantom AST nullable check. + if (k == nkind.N_TTAGGED) { + let pa: *node = aa.list; + let pb: *node = bb.list; + for (pa != nil) { + if (pb == nil) { return false; }; + // #115: a `...inner` spread variant stays unflattened at + // this AST layer (casevariantin flattens only at the OUTER + // level; cstage flattens in resolve_type before type_eq ever + // runs). A position-by-position compare cannot honour it — + // the N_TNAME leg is pure streq, so `...ab` would silently + // match a plain `ab`, accepting a case cstage rejects. + // Conservatively loud-reject any spread until the flatten + // lands; b1c's (void|size) has none, so byte-id is untouched. + if (pa.op == tkind.TK_ELLIPSIS) { return false; }; + if (pb.op == tkind.TK_ELLIPSIS) { return false; }; + if (!typeeqast(pa, pb)) { return false; }; + pa = pa.next; + pb = pb.next; + }; + return pb == nil; + }; + // Conservative: anything else (struct/array) fails the cheap + // check. Selfhost code doesn't currently rely on equality at + // these shapes for the targeted checks. return false; }; diff --git a/test/wcc/944_tuple_tagged_union_run.c b/test/wcc/944_tuple_tagged_union_run.c index 1766d2fd..45c3b0b7 100644 --- a/test/wcc/944_tuple_tagged_union_run.c +++ b/test/wcc/944_tuple_tagged_union_run.c @@ -18,17 +18,15 @@ * * Both stages get the SAME arm → byte-identical asm (rule 10). * - * K_RUN rows (ret-shape: construct + return + `is error` consume, NO match - * case-arm): build+run exit==want on BOTH drivers AND cs==ww byte-id. These - * are the gap-A pin — wwstage's store correctness rides on byte-id with the - * cstage runtime reference until gap-B unlocks wwstage's match-extract. - * - * K_CSRUN row (full b1c: construct + return + MATCH case-arm + is/as elem - * reads): build+run exit==want on CSTAGE ONLY. cstage's checker accepts the - * tuple-with-tagged case pattern; wwstage's does NOT (#47 gap-B, a separate - * later commit — casevariantin/typeeqast align-up). So the wwstage match- - * extract RUNTIME row lands with gap-B; here it is cstage-only and proves - * the recursive store reads back every tuple-elem value correctly. + * All rows are K_RUN: build+run exit==want on BOTH drivers AND cs==ww + * byte-id. The ret-shape rows (construct + return + `is error` consume, NO + * match case-arm) were the gap-A pin — wwstage's store correctness rides on + * byte-id with the cstage runtime reference. The full-b1c row (construct + + * return + MATCH case-arm + is/as elem reads) was cstage-only under gap-A + * because wwstage's checker rejected the tuple-with-tagged case pattern; + * #47 gap-B taught typeeqast the N_TTAGGED arm (cmd/wcc/type.c:288-300 + * TY_TAGGED twin), so wwstage now ACCEPTS it and the row is promoted to + * both-stage K_RUN — that full-b1c byte-id IS the gap-B correctness proof. * * NNN<950, self-contained (/tmp, no imports), so rule-14's selfhost-sibling * race does not apply (903/940/945 precedent). Cites #47 gap-A. @@ -66,7 +64,6 @@ slurp_eq(const char *a, const char *b) } #define K_RUN 0 /* build+run BOTH drivers, exit==want, + cs==ww byte-id */ -#define K_CSRUN 1 /* build+run CSTAGE ONLY (wwstage match-extract = gap-B) */ struct row { const char *label; const char *src; int kind; int want; }; @@ -122,12 +119,18 @@ static const struct row rows[] = { " if (a is error) { return 1; };\n" " return 235;\n" "};\n", K_RUN, 235 }, - /* full b1c (cstage-only): construct + return + MATCH case-arm + is/as + /* full b1c (gap-B): construct + return + MATCH case-arm + is/as * elem reads on the boxed tuple. pr(0): t.0 is void (+1), t.1 is size * = 5 (+5), t.2*10 (+70) = 76. pr(1): t.0 is size==3 (+100), t.1 is - * void (+50), t.2 = 9 (+9) = 235. pr(2): error arm, strings.compare - * matches. Reads back EVERY tuple-elem value the recursive store wrote. - * wwstage cannot match the tuple-with-tagged case pattern yet (gap-B). */ + * void (+50), t.2 = 9 (+9) = 235. pr(2): error arm reads back the boxed + * error str (ptr+len) and checks its length (25). Reads back EVERY + * tuple-elem value the recursive store wrote. gap-B taught wwstage's + * typeeqast the N_TTAGGED arm, so its checker now ACCEPTS this tuple- + * with-tagged case pattern — both-stage K_RUN (build+run 235 + cs==ww + * byte-id) is the gap-B correctness proof. Self-contained (no import, + * per this file's invariant): the error payload is verified with the + * len() builtin over an ident-bound str (a cast-expr len() operand is + * not place-resolvable, #10/#41) rather than a strings.compare import. */ { "b1c_full", "package main;\n" "type error = !str;\n" @@ -144,7 +147,6 @@ static const struct row rows[] = { " };\n" " return \"Negative repetition count\": error;\n" "};\n" - "import strings;\n" "export fn main() i32 = {\n" " let acc: size = 0;\n" " let a: (((void | size), (void | size), size) | error) = pr(0);\n" @@ -169,24 +171,20 @@ static const struct row rows[] = { " match (c) {\n" " case let t: ((void | size), (void | size), size) => { return 252; };\n" " case let e: error => {\n" - " if (strings.compare((e: str),\n" - " \"Negative repetition count\") != 0) { return 253; };\n" + " let s: str = e: str;\n" + " if (len(s) != 25) { return 253; };\n" " };\n" " };\n" " return (acc: i32);\n" - "};\n", K_CSRUN, 235 }, + "};\n", K_RUN, 235 }, }; -/* build+run via a driver; returns 0 pass, nonzero fail. is_ww gates the - * cstage-only K_CSRUN row. */ +/* build+run via a driver; returns 0 pass, nonzero fail. */ static int -run_driver(const char *driver, int is_ww, const struct row *r, int i) +run_driver(const char *driver, const struct row *r, int i) { char src[96], tmpdir[96], errf[96], cmd[1024]; - if (r->kind == K_CSRUN && is_ww) - return 0; /* wwstage match-extract is gap-B */ - snprintf(src, sizeof src, "/tmp/ttu_%d_%d.ww", getpid(), i); snprintf(tmpdir, sizeof tmpdir, "/tmp/ttu_%d_d_%d", getpid(), i); snprintf(errf, sizeof errf, "/tmp/ttu_%d_e_%d", getpid(), i); @@ -274,11 +272,11 @@ main(void) snprintf(w6c, sizeof w6c, "%s/w6c", bin); snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); - struct { const char *name; const char *path; int gated; int is_ww; } + struct { const char *name; const char *path; int gated; } drivers[] = { - { "cstage", cdrv, 0, 0 }, - { "wwstage", wdrv, 1, 1 }, - { NULL, NULL, 0, 0 }, + { "cstage", cdrv, 0 }, + { "wwstage", wdrv, 1 }, + { NULL, NULL, 0 }, }; int n = (int)(sizeof rows / sizeof rows[0]); @@ -292,13 +290,13 @@ main(void) } for (int i = 0; i < n; i++) { total++; - if (run_driver(drivers[d].path, drivers[d].is_ww, - &rows[i], i) != 0) fail++; + if (run_driver(drivers[d].path, &rows[i], i) != 0) + fail++; } } - /* cs==ww byte-id for K_RUN rows only (K_CSRUN's match arm has no - * wwstage .s until gap-B). */ + /* cs==ww byte-id for every K_RUN row (gap-B promoted the full-b1c + * match-extract row, so all rows now emit wwstage .s). */ if (access(w6c_ww, X_OK) == 0) { for (int i = 0; i < n; i++) { if (rows[i].kind != K_RUN) continue;