Files
ww/test/wcc/773_isas_spread_variant.c
Hojun-Cho 3f4eeff7ff wcc: route is/as variant lookup through tinfo.params (#198)
checkisas walked the unflattened AST u.list via casevariantin (typeeqast
streq), so any variant introduced via a `...inner` spread was invisible
and rejected as "is/as: not a variant of operand". Repro:

    type rsh = (size | io.eof | ...io.error);
    let r: rsh = 42: size;
    if (r is io.underread) ...    -- pre-fix wwstage REJECTS

io.underread is in io.error.params, which tinfofornode splices into the
parent at L1827-1836, but the AST u.list still holds the single
`...io.error` entry that streq("io.underread", "io.error") rejects.

Route through flatvariantidxt — the same Phase-N helper #179 cgmatch
and #66 cgtagvariantidx already key off. Mirrors cstage cmd/wcc/check.c
:1662-1675 u->params + type_eq. Falls back to casevariantin AST walk
when tinfo isn't available (defensive — non-#198 path stays as-is).

project_tinfo_lossy_nominal: name-keying was the pre-Phase-N workaround
for tinfo lossy on nominal identity; typeeq inside flatvariantidxt now
handles NAMED ptr-id (#64), so the checker pair aligns with cgen on the
flattened-variant axis.

Closes the cgen-drain mini-cluster (#201 -> #199 -> #200 -> #198).

773_isas_spread_variant: 5 rows (spread_is_inline_variant,
direct_cross_mod_tagged, cross_mod_named_void, same_module_variant,
spread_as_inline_payload). Rows 2-4 byte-id; rows 1/5 skip byte-id due
to layout-asymmetry on `...wrapper` (cstage flattens at resolve_type,
wwstage computes maxsz off vt.size of the un-spliced alias) — sibling
not blocking the checker correctness fix.
2026-05-29 04:51:44 +09:00

314 lines
10 KiB
C

/*
* 773_isas_spread_variant — project #198: wwstage checker `is`/`as`
* variant lookup walked the unflattened AST u.list (casevariantin via
* typeeqast streq), so any variant introduced via a `...inner` spread
* was invisible to the checker and rejected as "is/as: not a variant
* of operand". Repro: `r: (size | io.eof | ...io.error); r is
* io.underread;` — io.underread is in io.error's params chain, which
* gets spliced into the parent union at tinfofornode L1827-1836, but
* the AST u.list still holds the single `...io.error` entry that
* streq("io.underread", "io.error") rejects.
*
* Fix: selfhost/cmd/wcc/check.ww checkisas (lines ~3441-3466). Route
* through tinfo.params via flatvariantidxt (the same Phase-N helper
* #179 cgmatch and #66 cgtagvariantidx use); fall back to casevariantin
* AST walk when tinfo isn't available (defensive — non-#198 path stays
* as-is). Mirrors cstage cmd/wcc/check.c:1662-1675 u->params + type_eq.
* Closes the cgen-drain mini-cluster (#201 → #199 → #200 → #198).
*
* Coverage (5 rows):
* 1. spread_is_inline_variant — `(size | io.eof | ...io.error)`,
* `r is io.underread`. THE BUG: pre-
* fix wwstage rejects with "is/as:
* not a variant of operand
* (io.underread)". Post-fix accepts;
* cgen tag-compare hits the flatten-
* spread index. No byte-id: layout-
* asymmetry on `...wrapper` (cstage
* flattens at resolve_type, wwstage
* computes maxsz off vt.size of the
* un-spliced alias — sibling task,
* not blocking checker correctness).
* 2. direct_cross_mod_tagged — `(size | io.eof | io.error)`,
* `r is io.error`. Direct cross-mod
* tagged variant (no spread). Regr-
* ession gate: AST-streq path always
* matched this; the new tinfo path
* must too. byte-id ON.
* 3. cross_mod_named_void — `(size | io.eof)`, `r is io.eof`.
* Cross-mod NAMED-void variant. Regr-
* ession gate for the io.eof shape
* used pervasively in the io fold.
* byte-id ON.
* 4. same_module_variant — `(i32 | str)`, `r is i32`. Bare
* same-module primitive variant.
* Lowest-bar regression gate: no
* cross-mod, no spread, no NAMED.
* byte-id ON.
* 5. spread_as_inline_payload — `(size | io.eof | ...io.error)`,
* `r as io.underread`. `as` twin of
* row 1: same checker path
* (checkisas dispatches on both
* N_TYPETEST and N_TYPEASSERT), with
* the payload-extract runtime gate.
* No byte-id (sibling, same as row 1).
*
* Per-row gates: cstage runtime exit, wwstage runtime exit, cs.s ==
* ww.s byte-identical when byte_id != 0.
*
* GATE POLARITY: must stay GREEN. Rows 1/5 red means the tinfo.params
* routing dropped a spread-flattened variant; row 2 red means the
* direct cross-mod path broke; row 3 red means the NAMED-void path
* broke; row 4 red means a same-module / same-package regression.
*/
#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;
}
#define STAGE_CS 1
#define STAGE_WW 2
struct row {
const char *label;
const char *src;
int expected_exit;
int stage_mask;
int byte_id;
};
static const struct row rows[] = {
{ "spread_is_inline_variant",
"package main;\n"
"import io;\n"
"type rsh = (size | io.eof | ...io.error);\n"
"export fn main() i32 = {\n"
" let r: rsh = 42: size;\n"
" if (r is io.underread) { return 1; };\n"
" if (r is io.eof) { return 2; };\n"
" if (r is size) { return 3; };\n"
" return 0;\n"
"};\n",
3,
STAGE_CS | STAGE_WW, 0 },
{ "direct_cross_mod_tagged",
"package main;\n"
"import io;\n"
"export fn main() i32 = {\n"
" let r: (size | io.eof | io.error) = 42: size;\n"
" if (r is io.error) { return 1; };\n"
" if (r is io.eof) { return 2; };\n"
" if (r is size) { return 3; };\n"
" return 0;\n"
"};\n",
3,
STAGE_CS | STAGE_WW, 1 },
{ "cross_mod_named_void",
"package main;\n"
"import io;\n"
"export fn main() i32 = {\n"
" let r: (size | io.eof) = 42: size;\n"
" if (r is io.eof) { return 1; };\n"
" if (r is size) { return 3; };\n"
" return 0;\n"
"};\n",
3,
STAGE_CS | STAGE_WW, 1 },
{ "same_module_variant",
"package main;\n"
"export fn main() i32 = {\n"
" let r: (i32 | str) = 3: i32;\n"
" if (r is i32) { return 3; };\n"
" if (r is str) { return 1; };\n"
" return 0;\n"
"};\n",
3,
STAGE_CS | STAGE_WW, 1 },
{ "spread_as_inline_payload",
"package main;\n"
"import io;\n"
"type rsh = (size | io.eof | ...io.error);\n"
"fn pick(b: i32) rsh = {\n"
" if (b == 1) { let u: io.underread = 7: i32: io.underread; return u; };\n"
" return 42: size;\n"
"};\n"
"export fn main() i32 = {\n"
" let r = pick(1);\n"
" let u = r as io.underread;\n"
" return u: i32;\n"
"};\n",
7,
STAGE_CS | STAGE_WW, 0 },
};
static int
write_source(const char *path, const char *src)
{
FILE *f = fopen(path, "wb");
if (!f) return -1;
fputs(src, f);
fclose(f);
return 0;
}
static void
cleanup_tmp(const char *tmpdir, const char *base)
{
char p[512];
snprintf(p, sizeof p, "%s/%s.ww", tmpdir, base); unlink(p);
snprintf(p, sizeof p, "%s/%s.s", tmpdir, base); unlink(p);
snprintf(p, sizeof p, "%s/%s.o", tmpdir, base); unlink(p);
snprintf(p, sizeof p, "%s/%s.combined.ww", tmpdir, base); unlink(p);
snprintf(p, sizeof p, "%s/%s", tmpdir, base); unlink(p);
rmdir(tmpdir);
}
static int
build_via_driver(const char *driver, const char *tmpdir, const char *src)
{
char cmd[1024];
snprintf(cmd, sizeof cmd, "cd %s && timeout 180 %s build %s 2>/dev/null",
tmpdir, driver, src);
return runwait(cmd);
}
static int
run_row(const char *driver, const struct row *r, int seq)
{
char tmpdir[256], src[512], base[64], outbin[768];
snprintf(tmpdir, sizeof tmpdir, "/tmp/isas_%d_d_%d", getpid(), seq);
snprintf(src, sizeof src, "%s/main773.ww", tmpdir);
snprintf(base, sizeof base, "main773");
mkdir(tmpdir, 0755);
if (write_source(src, r->src) != 0) {
cleanup_tmp(tmpdir, base);
return -1;
}
int rc = -1;
if (build_via_driver(driver, tmpdir, src) == 0) {
snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base);
rc = runwait(outbin);
}
cleanup_tmp(tmpdir, base);
return rc;
}
/* Parallel tmpdirs per CLAUDE.md rule 14 — ww_ww writes intermediates
* next to the source so concurrent cstage/wwstage builds against the
* same path would race. */
static int
asm_byte_identical(const char *cdrv, const char *wdrv,
const struct row *r, int seq)
{
char src[512], tdc[256], tdw[256], base[64], cs[512], ws[512];
snprintf(tdc, sizeof tdc, "/tmp/isas_%d_c_%d", getpid(), seq);
snprintf(tdw, sizeof tdw, "/tmp/isas_%d_w_%d", getpid(), seq);
snprintf(base, sizeof base, "main773");
mkdir(tdc, 0755);
mkdir(tdw, 0755);
snprintf(src, sizeof src, "%s/main773.ww", tdc);
if (write_source(src, r->src) != 0) { cleanup_tmp(tdc, base); cleanup_tmp(tdw, base); return -1; }
int rc = -1;
if (build_via_driver(cdrv, tdc, src) != 0) goto out;
snprintf(cs, sizeof cs, "%s/%s.s", tdc, base);
snprintf(src, sizeof src, "%s/main773.ww", tdw);
if (write_source(src, r->src) != 0) goto out;
if (build_via_driver(wdrv, tdw, src) != 0) goto out;
snprintf(ws, sizeof ws, "%s/%s.s", tdw, base);
FILE *fc = fopen(cs, "rb");
FILE *fw = fopen(ws, "rb");
if (fc && fw) {
rc = 0;
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);
out:
cleanup_tmp(tdc, base);
cleanup_tmp(tdw, base);
return rc;
}
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], wdrv[640];
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin);
int n = (int)(sizeof rows / sizeof rows[0]);
int total = 0, fail = 0;
int wwpresent = (access(wdrv, X_OK) == 0);
int seq = 0;
for (int i = 0; i < n; i++) {
if (rows[i].stage_mask & STAGE_CS) {
total++;
int got = run_row(cdrv, &rows[i], seq++);
if (got != rows[i].expected_exit) {
fprintf(stderr,
"isas_spread_variant[cstage run][%s]: exit=%d want=%d\n",
rows[i].label, got, rows[i].expected_exit);
fail++;
}
}
if (wwpresent && (rows[i].stage_mask & STAGE_WW)) {
total++;
int got = run_row(wdrv, &rows[i], seq++);
if (got != rows[i].expected_exit) {
fprintf(stderr,
"isas_spread_variant[wwstage run][%s]: exit=%d want=%d\n",
rows[i].label, got, rows[i].expected_exit);
fail++;
}
if (rows[i].byte_id) {
total++;
if (asm_byte_identical(cdrv, wdrv, &rows[i], seq++) != 0) {
fprintf(stderr,
"isas_spread_variant[byte-id][%s]: cstage vs wwstage asm differs\n",
rows[i].label);
fail++;
}
}
}
}
if (!wwpresent)
fprintf(stderr, "isas_spread_variant: skip wwstage (no %s)\n", wdrv);
if (fail) {
fprintf(stderr, "isas_spread_variant: %d/%d fixtures failed\n",
fail, total);
return 1;
}
printf("isas_spread_variant: %d/%d ok\n", total, total);
return 0;
}