selfhost/cmd/wcc: type-key tagged variant match (#66, Phase-N step 3)

The user-ruled B-full semantic change: flip tagged-union variant matching
from surface-NAME to TYPE-identity (typeeq over tinfo.params), mirroring
cstage cg_variant_match (cmd/w6c/cgen.c:451). A cross-module `a.T` != `b.T`
and `type linerr=!str` != str are now distinguished by the per-decl TY_NAMED
pointer (Phase-N #64). ww has no type_assignable, so the untyped/loose arm
keeps the str/slice shape fallback (rule-10 align-down). The 5 helpers
(flatvariantidx, flatslicevariantidx, taggedvariantindex, cgtagvariantidx,
cgmatch dispatch) flip; nomem propagation (NAMED-name scan, no source value)
and the f64 widen arm (float-kind classification, no pattern node) are not
arm-by-value discrimination and stay name/kind-keyed.

The flip requires value nodes to carry nominal identity. exprtype's
N_STRUCTLIT arm stamped the flattened body, so `overflow{}` (overflow=!void)
got TY_VOID and missed its variant -- fixed to stamp the per-decl NAMED
(mktname(lhs.str) -> tinfofornode reuses the #64 NAMED build/cache, same ptr
the union variant resolved to), mirroring the N_CAST/N_IDENT arms + cstage.
Returns the body node unchanged (only e.type_ rides NAMED); struct-lit layout
is unaffected -- cgstructlitfill is structlookup(name)-keyed, never reads
NAMED.fields. The fix now hits all `T{}` stamps, kept byte-id by the #63/#65
structural-walker peels.

931_variant_typekey_run: table-driven, both stages, /tmp-isolated. Two rows
widen an alias-FIRST variant from a call (no surface name): `(linerr|str)`
str-via-call -> idx 1, `(ec|i32)` i32-via-call -> idx 1. Empirically
discriminating: FAILS pre-flip (wwstage falls to the leading-shape variant,
exit 10; cstage exit 0) and PASSES post-flip -- locking in the capability
byte-id can't reach (the corpus has no name-key/type-key-disagreeing
co-variant, which is why name-keying survived).

make test 134/134 (byte-id 990-997 green; 995 self-rebuild green).
This commit is contained in:
2026-05-23 21:22:24 +09:00
parent 0fde4beeb4
commit 3036ba766d
8 changed files with 634 additions and 369 deletions

View File

@@ -0,0 +1,198 @@
/*
* 931_variant_typekey_run — Class B semantic sentinel for #66
* (Phase-N step 3, the user-ruled type-keying flip).
*
* Locks in the behavioral capability the corpus does NOT exercise:
* distinguishing two co-variants that share a SHAPE (and, pre-flip,
* a name key) by NOMINAL TYPE identity. cstage's cg_variant_match
* (cmd/w6c/cgen.c:451) is what keeps `(str | linerr)` separable even
* though `type linerr = !str` unwraps to str; #66 brings wwstage's
* flatvariantidx to the same typeeq discipline (per-decl TY_NAMED ptr).
*
* Discriminating shape: a tagged union with the alias variant FIRST,
* widened from a value with no surface type name (a call return).
* Pre-#66 wwstage routed the value→tag direction through rhstargetname
* (surface name) + a str/scalar shape fallback that picked the FIRST
* shape-matching variant — i.e. the leading alias — so a plain `str`
* from a call was tagged as `linerr` (idx 0) instead of `str` (idx 1).
* A subsequent match then fired the wrong arm. cstage always type-keyed,
* so pre-#66 this was a cstage-vs-wwstage divergence; each row exits 10
* on pre-#66 wwstage and 0 on cstage / post-#66 wwstage. The 990-997
* byte-id gates do not cover this because the corpus has no
* same-shape co-variant whose name key and type key disagree.
*
* Pure runtime test: build through both drivers (cstage `ww`, wwstage
* `ww_ww`) and assert each arm fires for its matching input.
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
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; /* single-module program — written as p.ww */
int want;
};
static const struct row rows[] = {
/* str axis. `(linerr | str)` with linerr = !str, alias first.
* classify(0) widens a str returned by a call (no surface name)
* — pre-#66 the shape fallback tags it as the leading str-shape
* variant linerr (idx 0) and the match returns 1 not 2.
* classify(1) widens a linerr (round-trip soundness). */
{ "linerr_str_alias_first_callret",
"package main;\n"
"type linerr = !str;\n"
"fn getstr() str = { return \"hi\"; };\n"
"fn produce(k: i32) (linerr | str) = {\n"
" if (k == 0) { return getstr(); };\n"
" let e: linerr = \"boom\";\n"
" return e;\n"
"};\n"
"export fn classify(k: i32) i32 = {\n"
" match (produce(k)) {\n"
" case let er: linerr => return 1;\n"
" case let v: str => return 2;\n"
" };\n"
"};\n"
"export fn main() i32 = {\n"
" if (classify(0) != 2) { return 10; };\n"
" if (classify(1) != 1) { return 11; };\n"
" return 0;\n"
"};\n",
0 },
/* scalar axis. `(ec | i32)` with ec = !i32, alias first.
* classify(0) widens an i32 returned by a call — pre-#66 the
* scalar shape fallback tags it as the leading scalar-shape
* variant ec (idx 0). Confirms the flip isn't str-specific. */
{ "errint_int_alias_first_callret",
"package main;\n"
"type ec = !i32;\n"
"fn getint() i32 = { return 7; };\n"
"fn produce(k: i32) (ec | i32) = {\n"
" if (k == 0) { return getint(); };\n"
" let e: ec = 9;\n"
" return e;\n"
"};\n"
"export fn classify(k: i32) i32 = {\n"
" match (produce(k)) {\n"
" case let er: ec => return 1;\n"
" case let v: i32 => return 2;\n"
" };\n"
"};\n"
"export fn main() i32 = {\n"
" if (classify(0) != 2) { return 10; };\n"
" if (classify(1) != 1) { return 11; };\n"
" return 0;\n"
"};\n",
0 },
};
static int
write_file(const char *dir, const char *name, const char *body)
{
char path[512];
snprintf(path, sizeof path, "%s/%s", dir, name);
FILE *f = fopen(path, "wb");
if (!f) return -1;
fputs(body, f);
fclose(f);
return 0;
}
static int
run_driver(const char *driver, const struct row *r, int i)
{
char tmpdir[96], cmd[2048];
snprintf(tmpdir, sizeof tmpdir, "/tmp/vtk_%d_%d", getpid(), i);
mkdir(tmpdir, 0755);
if (write_file(tmpdir, "p.ww", r->src) != 0) return -1;
snprintf(cmd, sizeof cmd,
"cd %s && %s build p.ww 2>/dev/null", tmpdir, driver);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: build via %s failed\n",
r->label, driver);
snprintf(cmd, sizeof cmd, "rm -rf %s", tmpdir);
runwait(cmd);
return -1;
}
char outbin[160];
snprintf(outbin, sizeof outbin, "%s/p", tmpdir);
int got = runwait(outbin);
snprintf(cmd, sizeof cmd, "rm -rf %s", tmpdir);
runwait(cmd);
return got;
}
int
main(void)
{
const char *bin = getenv("BIN");
if (!bin) bin = "out/bin";
char absbin[512];
if (bin[0] != '/') {
char cwd[256];
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
bin = absbin;
}
char cdrv[640];
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
char wdrv[640];
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,
"variant_typekey_run: 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,
"variant_typekey_run[%s][%s]: exit=%d want=%d\n",
drivers[d].name, rows[i].label,
got, rows[i].want);
fail++;
}
}
}
if (fail) {
fprintf(stderr,
"variant_typekey_run: %d/%d fixtures failed\n",
fail, total);
return 1;
}
printf("variant_typekey_run: %d/%d ok\n", total, total);
return 0;
}