wcc_ww/check: #47 gap-B tuple-with-tagged case-arm variant-match (align-up)

wwstage's checker rejected a `case let t: ((void|size),(void|size),
size) =>` arm against a (tuple|error) scrutinee ("case: not a variant
of scrutinee"), while cstage accepts and runs it. typeeqast's N_TTUPLE
arm recurses per-element, but a tagged element (void|size) is
N_TTAGGED -> fell to the conservative catch-all `return false`, so the
whole tuple-compare failed. typeeqast is the sole acceptance route
(casevariantpairmatch is N_TNAME-only).

Add an N_TTAGGED arm to typeeqast, sibling of N_TTUPLE, mirroring
cstage type.c:288-300 (type_eq TY_TAGGED): position-by-position
variant compare over the tagged node's .list (direct nodes, not
.lhs-wrapped). cstage's nullable-flag check is deliberately not ported
(resolved-Type property, no ww AST analogue; moot for case-match).

A spread variant (TK_ELLIPSIS) in the .list is loud-rejected rather
than compared: a naive streq would silently accept a `...ab` case that
cstage rejects (a new cs!=ww over-accept the bare arm introduced).
Flattening the spread is deferred (#115); until then it louds, matching
cstage.

ww-only (cstage already accepts); the gap-A cgen store landed in
6a5bb3e. wwstage now accepts the b1c match and runs the full shape
byte-identical to cstage -> #47 (both gaps) closed. The deferred
full-b1c row in 944_tuple_tagged_union_run is promoted to a both-stage
runtime row. Checker change is acceptance-only/additive -> bootstrap
byte-id neutral (990-997 green).
This commit is contained in:
2026-06-06 17:36:10 +09:00
parent 6a5bb3efc9
commit da30f10f70
4 changed files with 134 additions and 43 deletions

View File

@@ -11174,9 +11174,40 @@ fn typeeqast(a: *node, b: *node) bool = {
}; };
return pb == nil; return pb == nil;
}; };
// Conservative: anything else (struct/tagged/array) fails the // #47 gap-B: tagged structural equality — mirror of cstage
// cheap check. Selfhost code doesn't currently rely on equality // type.c:288-300 (TY_TAGGED). A tuple member with a TAGGED element
// at these shapes for the targeted checks. // (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; return false;
}; };

View File

@@ -893,9 +893,40 @@ fn typeeqast(a: *node, b: *node) bool = {
}; };
return pb == nil; return pb == nil;
}; };
// Conservative: anything else (struct/tagged/array) fails the // #47 gap-B: tagged structural equality — mirror of cstage
// cheap check. Selfhost code doesn't currently rely on equality // type.c:288-300 (TY_TAGGED). A tuple member with a TAGGED element
// at these shapes for the targeted checks. // (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; return false;
}; };

View File

@@ -11174,9 +11174,40 @@ fn typeeqast(a: *node, b: *node) bool = {
}; };
return pb == nil; return pb == nil;
}; };
// Conservative: anything else (struct/tagged/array) fails the // #47 gap-B: tagged structural equality — mirror of cstage
// cheap check. Selfhost code doesn't currently rely on equality // type.c:288-300 (TY_TAGGED). A tuple member with a TAGGED element
// at these shapes for the targeted checks. // (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; return false;
}; };

View File

@@ -18,17 +18,15 @@
* *
* Both stages get the SAME arm → byte-identical asm (rule 10). * Both stages get the SAME arm → byte-identical asm (rule 10).
* *
* K_RUN rows (ret-shape: construct + return + `is error` consume, NO match * All rows are K_RUN: build+run exit==want on BOTH drivers AND cs==ww
* case-arm): build+run exit==want on BOTH drivers AND cs==ww byte-id. These * byte-id. The ret-shape rows (construct + return + `is error` consume, NO
* are the gap-A pin — wwstage's store correctness rides on byte-id with the * match case-arm) were the gap-A pin — wwstage's store correctness rides on
* cstage runtime reference until gap-B unlocks wwstage's match-extract. * 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
* K_CSRUN row (full b1c: construct + return + MATCH case-arm + is/as elem * because wwstage's checker rejected the tuple-with-tagged case pattern;
* reads): build+run exit==want on CSTAGE ONLY. cstage's checker accepts the * #47 gap-B taught typeeqast the N_TTAGGED arm (cmd/wcc/type.c:288-300
* tuple-with-tagged case pattern; wwstage's does NOT (#47 gap-B, a separate * TY_TAGGED twin), so wwstage now ACCEPTS it and the row is promoted to
* later commit — casevariantin/typeeqast align-up). So the wwstage match- * both-stage K_RUN — that full-b1c byte-id IS the gap-B correctness proof.
* extract RUNTIME row lands with gap-B; here it is cstage-only and proves
* the recursive store reads back every tuple-elem value correctly.
* *
* NNN<950, self-contained (/tmp, no imports), so rule-14's selfhost-sibling * 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. * 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_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; }; 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" " if (a is error) { return 1; };\n"
" return 235;\n" " return 235;\n"
"};\n", K_RUN, 235 }, "};\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 * 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 * = 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 * void (+50), t.2 = 9 (+9) = 235. pr(2): error arm reads back the boxed
* matches. Reads back EVERY tuple-elem value the recursive store wrote. * error str (ptr+len) and checks its length (25). Reads back EVERY
* wwstage cannot match the tuple-with-tagged case pattern yet (gap-B). */ * 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", { "b1c_full",
"package main;\n" "package main;\n"
"type error = !str;\n" "type error = !str;\n"
@@ -144,7 +147,6 @@ static const struct row rows[] = {
" };\n" " };\n"
" return \"Negative repetition count\": error;\n" " return \"Negative repetition count\": error;\n"
"};\n" "};\n"
"import strings;\n"
"export fn main() i32 = {\n" "export fn main() i32 = {\n"
" let acc: size = 0;\n" " let acc: size = 0;\n"
" let a: (((void | size), (void | size), size) | error) = pr(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" " match (c) {\n"
" case let t: ((void | size), (void | size), size) => { return 252; };\n" " case let t: ((void | size), (void | size), size) => { return 252; };\n"
" case let e: error => {\n" " case let e: error => {\n"
" if (strings.compare((e: str),\n" " let s: str = e: str;\n"
" \"Negative repetition count\") != 0) { return 253; };\n" " if (len(s) != 25) { return 253; };\n"
" };\n" " };\n"
" };\n" " };\n"
" return (acc: i32);\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 /* build+run via a driver; returns 0 pass, nonzero fail. */
* cstage-only K_CSRUN row. */
static int 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]; 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(src, sizeof src, "/tmp/ttu_%d_%d.ww", getpid(), i);
snprintf(tmpdir, sizeof tmpdir, "/tmp/ttu_%d_d_%d", getpid(), i); snprintf(tmpdir, sizeof tmpdir, "/tmp/ttu_%d_d_%d", getpid(), i);
snprintf(errf, sizeof errf, "/tmp/ttu_%d_e_%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, sizeof w6c, "%s/w6c", bin);
snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", 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[] = { drivers[] = {
{ "cstage", cdrv, 0, 0 }, { "cstage", cdrv, 0 },
{ "wwstage", wdrv, 1, 1 }, { "wwstage", wdrv, 1 },
{ NULL, NULL, 0, 0 }, { NULL, NULL, 0 },
}; };
int n = (int)(sizeof rows / sizeof rows[0]); int n = (int)(sizeof rows / sizeof rows[0]);
@@ -292,13 +290,13 @@ main(void)
} }
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
total++; total++;
if (run_driver(drivers[d].path, drivers[d].is_ww, if (run_driver(drivers[d].path, &rows[i], i) != 0)
&rows[i], i) != 0) fail++; fail++;
} }
} }
/* cs==ww byte-id for K_RUN rows only (K_CSRUN's match arm has no /* cs==ww byte-id for every K_RUN row (gap-B promoted the full-b1c
* wwstage .s until gap-B). */ * match-extract row, so all rows now emit wwstage .s). */
if (access(w6c_ww, X_OK) == 0) { if (access(w6c_ww, X_OK) == 0) {
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
if (rows[i].kind != K_RUN) continue; if (rows[i].kind != K_RUN) continue;