wcc/check: A7 honest-floor tagged-subset reject closes wide→narrow miscompile (wwstage)

wwstage's tagged→tagged subset-assign arm accepted all (lenient escape), silently miscompiling implicit wide→narrow: a (int|bool|str) holding a str, assigned to (int|bool), ran the int arm and read the str pointer as int. cstage rejects loud; this escape was the lone divergence.

Replace the escape with cstage's subset walk (src ⊆ dst): a src variant is covered iff typeeqast matches (structural — []u8/nested/primitives) OR both are N_TNAME with equal leaf names. The leaf bridge covers cross-module forwards where a callee's bare inline-union variant (utf8's `done`) meets the consumer's qualified `utf8.done` — raw typeeqast can't, and the escape was masking it for every forward. Mirrors casecovers (check.ww:4136). cstage untouched (align-up); spread-bearing unions keep the escape (#199b orthogonal).

Honest floor: leaf-only defers true module identity to #10 — a callee's defining module for an inline-union return is unrecoverable at check-time (the call node is gone; #211 fnretlookupmod is cgen-only). Retained divergence = cross-module same-leaf-collision over-accept (absent from bootstrap), documented at the site and filed as #10-A7 / census cat-A.

Test 989_tagged_subset_reject is table-driven with composition-discriminating rows: []u8 subset (typeeqast-only), bare↔qualified xmod forward (leaf-only), xmod named genuine-absence (reject). 352 green; w6c unchanged, w6c_ww 4b4496b8→4b316f01.
This commit is contained in:
2026-06-10 00:22:22 +09:00
parent 758d3ec8c3
commit fddd167ce8
5 changed files with 648 additions and 3 deletions

View File

@@ -244,6 +244,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
$(BIN)/test_arr_tagged_elem \
$(BIN)/test_arr_infer_len \
$(BIN)/test_arr_cap_reject \
$(BIN)/test_tagged_subset_reject \
$(BIN)/test_arr_ptr_global \
$(BIN)/test_def_arr_infer_len \
$(BIN)/test_def_arr_len \
@@ -663,6 +664,12 @@ $(BIN)/test_arr_cap_reject: test/wcc/817_arr_cap_reject.c $(BIN)/ww \
$(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
$(BIN)/test_tagged_subset_reject: test/wcc/989_tagged_subset_reject.c $(BIN)/ww \
$(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
$(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \
$(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
$(BIN)/test_arr_ptr_global: test/wcc/818_arr_ptr_global.c $(BIN)/ww \
$(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
$(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \

View File

@@ -14243,7 +14243,57 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
};
v = v.next;
};
*confident = false;
// Spread `...` member on either side: keep the lenient escape.
// cstage flattens spreads at resolve_type so its subset loop
// never sees one; wwstage stays AST-keyed (#115, check.ww:921),
// so a TK_ELLIPSIS variant can reach here. Routing it through
// the strict typeeqast subset loop would over-reject a valid
// spread-widen cstage accepts → new cs≠ww divergence. Spread
// decl-form layout is #199b, deferred.
let hasspread: bool = false;
for (let p: *node = du.list; p != nil; p = p.next) {
if (p.op == tkind.TK_ELLIPSIS) { hasspread = true; };
};
for (let p: *node = su.list; p != nil; p = p.next) {
if (p.op == tkind.TK_ELLIPSIS) { hasspread = true; };
};
if (hasspread) {
*confident = false;
return true;
};
// Structural subset loop — wwstage was align-DOWN-missing this;
// cstage type.c type_assignable tagged→tagged arm (type.c:360-367).
// Every src variant must appear in dst, else a loud reject; a
// genuine subset accepts.
//
// Per-variant cover is the HONEST FLOOR (drew ruling A): typeeqast
// FIRST for the exact / structural variants (e.g. []u8 slices,
// nested unions — bufio scanbytes/scanline forward `[]u8`), THEN a
// LEAF-ONLY name bridge (qualleaf, module IGNORED) for the bare-vs-
// qualified spelling mix typeeqast cannot span. A callee returning
// an INLINE union spells its variants BARE (utf8.next:
// (rune|done|more|invalid)) while the consumer annotates them
// QUALIFIED (utf8.done); the module qualifier is unrecoverable for
// an inline-union return, so leaf alone decides. Mirrors casecovers'
// typeeqast-then-leaf composition (check.ww:4136). qualmod is NOT
// compared (cf casevariantpairmatch): module identity is the #10
// gap. Sound for the bootstrap — no two of its variants share a leaf
// (drew); the cross-module same-leaf collision (a.foo vs b.foo) is a
// known over-accept deferred to #10 / filed in the #4 census.
for (let sp: *node = su.list; sp != nil; sp = sp.next) {
let ok: bool = false;
for (let dp: *node = du.list; dp != nil; dp = dp.next) {
if (typeeqast(c, dp, sp)) { ok = true; break; };
let su2: *node = unwrapbang(sp);
let du2: *node = unwrapbang(dp);
if (su2 != nil && du2 != nil &&
su2.kind == nkind.N_TNAME && du2.kind == nkind.N_TNAME &&
streq(qualleaf(su2.str), qualleaf(du2.str))) {
ok = true; break;
};
};
if (!ok) { return false; };
};
return true;
};
// tagged → non-tagged: requires `?` / `!` / match to project a

View File

@@ -3962,7 +3962,57 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
};
v = v.next;
};
*confident = false;
// Spread `...` member on either side: keep the lenient escape.
// cstage flattens spreads at resolve_type so its subset loop
// never sees one; wwstage stays AST-keyed (#115, check.ww:921),
// so a TK_ELLIPSIS variant can reach here. Routing it through
// the strict typeeqast subset loop would over-reject a valid
// spread-widen cstage accepts → new cs≠ww divergence. Spread
// decl-form layout is #199b, deferred.
let hasspread: bool = false;
for (let p: *node = du.list; p != nil; p = p.next) {
if (p.op == tkind.TK_ELLIPSIS) { hasspread = true; };
};
for (let p: *node = su.list; p != nil; p = p.next) {
if (p.op == tkind.TK_ELLIPSIS) { hasspread = true; };
};
if (hasspread) {
*confident = false;
return true;
};
// Structural subset loop — wwstage was align-DOWN-missing this;
// cstage type.c type_assignable tagged→tagged arm (type.c:360-367).
// Every src variant must appear in dst, else a loud reject; a
// genuine subset accepts.
//
// Per-variant cover is the HONEST FLOOR (drew ruling A): typeeqast
// FIRST for the exact / structural variants (e.g. []u8 slices,
// nested unions — bufio scanbytes/scanline forward `[]u8`), THEN a
// LEAF-ONLY name bridge (qualleaf, module IGNORED) for the bare-vs-
// qualified spelling mix typeeqast cannot span. A callee returning
// an INLINE union spells its variants BARE (utf8.next:
// (rune|done|more|invalid)) while the consumer annotates them
// QUALIFIED (utf8.done); the module qualifier is unrecoverable for
// an inline-union return, so leaf alone decides. Mirrors casecovers'
// typeeqast-then-leaf composition (check.ww:4136). qualmod is NOT
// compared (cf casevariantpairmatch): module identity is the #10
// gap. Sound for the bootstrap — no two of its variants share a leaf
// (drew); the cross-module same-leaf collision (a.foo vs b.foo) is a
// known over-accept deferred to #10 / filed in the #4 census.
for (let sp: *node = su.list; sp != nil; sp = sp.next) {
let ok: bool = false;
for (let dp: *node = du.list; dp != nil; dp = dp.next) {
if (typeeqast(c, dp, sp)) { ok = true; break; };
let su2: *node = unwrapbang(sp);
let du2: *node = unwrapbang(dp);
if (su2 != nil && du2 != nil &&
su2.kind == nkind.N_TNAME && du2.kind == nkind.N_TNAME &&
streq(qualleaf(su2.str), qualleaf(du2.str))) {
ok = true; break;
};
};
if (!ok) { return false; };
};
return true;
};
// tagged → non-tagged: requires `?` / `!` / match to project a

View File

@@ -14243,7 +14243,57 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
};
v = v.next;
};
*confident = false;
// Spread `...` member on either side: keep the lenient escape.
// cstage flattens spreads at resolve_type so its subset loop
// never sees one; wwstage stays AST-keyed (#115, check.ww:921),
// so a TK_ELLIPSIS variant can reach here. Routing it through
// the strict typeeqast subset loop would over-reject a valid
// spread-widen cstage accepts → new cs≠ww divergence. Spread
// decl-form layout is #199b, deferred.
let hasspread: bool = false;
for (let p: *node = du.list; p != nil; p = p.next) {
if (p.op == tkind.TK_ELLIPSIS) { hasspread = true; };
};
for (let p: *node = su.list; p != nil; p = p.next) {
if (p.op == tkind.TK_ELLIPSIS) { hasspread = true; };
};
if (hasspread) {
*confident = false;
return true;
};
// Structural subset loop — wwstage was align-DOWN-missing this;
// cstage type.c type_assignable tagged→tagged arm (type.c:360-367).
// Every src variant must appear in dst, else a loud reject; a
// genuine subset accepts.
//
// Per-variant cover is the HONEST FLOOR (drew ruling A): typeeqast
// FIRST for the exact / structural variants (e.g. []u8 slices,
// nested unions — bufio scanbytes/scanline forward `[]u8`), THEN a
// LEAF-ONLY name bridge (qualleaf, module IGNORED) for the bare-vs-
// qualified spelling mix typeeqast cannot span. A callee returning
// an INLINE union spells its variants BARE (utf8.next:
// (rune|done|more|invalid)) while the consumer annotates them
// QUALIFIED (utf8.done); the module qualifier is unrecoverable for
// an inline-union return, so leaf alone decides. Mirrors casecovers'
// typeeqast-then-leaf composition (check.ww:4136). qualmod is NOT
// compared (cf casevariantpairmatch): module identity is the #10
// gap. Sound for the bootstrap — no two of its variants share a leaf
// (drew); the cross-module same-leaf collision (a.foo vs b.foo) is a
// known over-accept deferred to #10 / filed in the #4 census.
for (let sp: *node = su.list; sp != nil; sp = sp.next) {
let ok: bool = false;
for (let dp: *node = du.list; dp != nil; dp = dp.next) {
if (typeeqast(c, dp, sp)) { ok = true; break; };
let su2: *node = unwrapbang(sp);
let du2: *node = unwrapbang(dp);
if (su2 != nil && du2 != nil &&
su2.kind == nkind.N_TNAME && du2.kind == nkind.N_TNAME &&
streq(qualleaf(su2.str), qualleaf(du2.str))) {
ok = true; break;
};
};
if (!ok) { return false; };
};
return true;
};
// tagged → non-tagged: requires `?` / `!` / match to project a

View File

@@ -0,0 +1,488 @@
/*
* 989_tagged_subset_reject — A7: wwstage align UP to cstage on the
* tagged-union → tagged-union assignability subset check (CHECKER-ONLY).
*
* THE BUG: an implicit (no-cast) WIDE → NARROW tagged assignment
* (`(int|bool|str)` returned where `(int|bool)` is wanted) was SILENTLY
* ACCEPTED by wwstage (its tagged→tagged arm fell through a lenient
* `*confident=false; return true` escape) while cstage correctly REJECTS
* it (structural subset loop, cmd/wcc/type.c:360-367). The accepted
* payload (`str`) was then read through the `int` arm by cgen → garbage
* → exit 1. Pure wwstage-align-UP: cstage gets ZERO touch.
*
* THE FIX (selfhost/cmd/wcc/check.ww, tagged→tagged arm): after the #205
* NAMED-variant nominal loop, replace the lenient escape with cstage's
* structural subset loop — every src variant must appear in dst (nominal
* identity via Layer-1 typeeqast); a missing variant rejects loud. GUARD:
* the subset loop runs ONLY when neither side carries a `...spread`
* (TK_ELLIPSIS) member; a spread-bearing union keeps the lenient escape
* (cstage flattens spreads in resolve, wwstage stays AST-keyed — routing
* a spread through the strict loop would over-reject a valid spread-widen
* cstage accepts; spread decl-form layout is #199b, deferred).
*
* The fix's predicate is a COMPOSITION (drew ruling A, the honest floor):
* per src variant, typeeqast FIRST (exact / structural — primitives, []u8
* slices, nested unions, Layer-1 nominal aliases), THEN a leaf-only name
* bridge (qualleaf, module ignored) for the bare-vs-qualified spelling mix
* an inline-union callee return forces (variants spelled BARE in the
* callee, QUALIFIED at the consumer; the module is unrecoverable there).
* The two single-file rows below cannot exercise either half distinctly
* (one module → every name is bare → typeeqast's streq matches), so the
* teeth for the COMPOSITION are the dedicated rows:
* - slice_struct_subset: a []u8 (N_TSLICE, not N_TNAME) src variant is
* matched ONLY by typeeqast — a pure-leaf-bridge regression would
* wrongly reject it.
* - xmod_inline_forward: a bare inline-union variant vs its qualified
* consumer spelling is matched ONLY by the leaf bridge (typeeqast /
* aliassym cannot resolve a bare inline-return variant in the
* consumer's scope) — dropping the bridge regresses cs=0/ww=1.
*
* row | src → dst | verdict
* -------------------------+---------------------------------+-----------
* wide_narrow_nocast | (int|bool|str) → (int|bool) | REJECT [bug]
* flatten_subset | (bool|str) → (int|bool|str) | ok 0 +byteid
* slice_struct_subset | (int|[]u8) → (int|bool|[]u8) | ok 0 +byteid
* nested_wrap | inner → (int|inner) | ok 42 +byteid
* concrete_spread_widen | 42,"hi" → (...formattable|bool) | ok 0
* spread_named_casematch | case e1 of (str|...e1) | REJECT [#115]
* spread_wider_subset | (str|...e1) → (str|bool|int|f64)| ok 0 [lenient]
*
* Cross-module rows (839-style -I build; wwstage-gated; byte-id NOT
* asserted — receiving + re-passing a cross-module tagged return crosses a
* pre-existing cgen frame-layout divergence, cf. 839):
* xmod_inline_forward | e.next (i64|done) → (i64|e.done)| ok 0
* xmod_inline_narrow | (i64|done|more) → (i64|e.done) | REJECT
*
* Rows flatten_subset / slice_struct_subset / nested_wrap also assert w6c
* vs w6c_ww asm is BYTE-IDENTICAL (rule-10): the fix only changes the
* REJECT decision and a confident-vs-lenient accept on inputs cstage
* already accepted, so the emitted code is unchanged. The two spread-accept
* rows (concrete_spread_widen / spread_wider_subset) assert accept-on-both-
* stages only — they ride the deferred #199b spread path where the stages
* intentionally diverge (wwstage AST-keyed vs cstage pre-flattened), so
* byte-id is NOT asserted (cf. 839's cross-module-return note).
* spread_named_casematch is a #115 case-match reject (not the subset arm),
* pinned here as a symmetric-both-stage regression guard.
*
* Both stages must agree (rule-10): REJECT rows build-FAIL on both; accept
* rows build+run to the same exit on cstage (`ww`) and wwstage (`ww_ww`).
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.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;
int expect_build; /* 1 = build+run to want_exit; 0 = must FAIL */
int want_exit; /* meaningful only when expect_build */
int byteid; /* 1 = also assert w6c vs w6c_ww .s byte-id */
};
static const struct row rows[] = {
/* (1) THE BUG — wide → narrow, no cast → REJECT both stages.
* `str` is a src variant absent from dst → subset loop rejects. */
{ "wide_narrow_nocast",
"package main;\n"
"type big = (int | bool | str);\n"
"type small = (int | bool);\n"
"fn f(b: big) small = { return b; };\n"
"export fn main() int = { return 0; };\n",
0, 0, 0 },
/* (2) D flatten-subset, union→union → ACCEPT + byte-id.
* {bool,str} ⊆ {int,bool,str}; cstage flattens inner + remaps tags. */
{ "flatten_subset",
"package main;\n"
"type inner = (bool | str);\n"
"type flat = (int | bool | str);\n"
"fn f(x: inner) flat = { return x; };\n"
"export fn main() int = { return 0; };\n",
1, 0, 1 },
/* (2b) []u8-bearing structural subset → ACCEPT + byte-id. The `[]u8`
* src variant is N_TSLICE, not N_TNAME, so it is matched ONLY by
* typeeqast — never the leaf bridge. Teeth for the composition: a
* pure-leaf-bridge predicate would drop typeeqast and wrongly reject
* this (cs=0/ww=1). {int,[]u8} ⊆ {int,bool,[]u8}. */
{ "slice_struct_subset",
"package main;\n"
"type a = (int | []u8);\n"
"type b = (int | bool | []u8);\n"
"fn f(x: a) b = { return x; };\n"
"export fn main() int = { return 0; };\n",
1, 0, 1 },
/* (3) B nested-wrap → ACCEPT + byte-id + runtime 42. inner is a
* DIRECT variant of outer → #205 nominal loop accepts (wrap, no
* flatten). Regression pin that the subset loop never reaches it. */
{ "nested_wrap",
"package main;\n"
"type inner = (bool | str);\n"
"type outer = (int | inner);\n"
"fn wrap(x: inner) outer = { return x; };\n"
"export fn main() int = {\n"
" let v: inner = \"hi\";\n"
" let o: outer = wrap(v);\n"
" match (o) {\n"
" case let i: int => { return 1; };\n"
" case let n: inner =>\n"
" match (n) {\n"
" case let b: bool => { return 2; };\n"
" case let s: str => { return 42; };\n"
" };\n"
" };\n"
"};\n",
1, 42, 1 },
/* (4) concrete → spread-field widen → ACCEPT (A6 concrete→tagged
* path, NOT the tagged→tagged subset arm). Pins the spread path is
* untouched; byte-id NOT asserted (#199b spread divergence). */
{ "concrete_spread_widen",
"package main;\n"
"type formattable = (int | str | bool);\n"
"type field = (...formattable | bool);\n"
"fn take(f: field) int = { return 0; };\n"
"export fn main() int = { take(42); take(\"hi\"); return 0; };\n",
1, 0, 0 },
/* (5) `...spread` member vs NAMED case pattern → REJECT both stages
* (#115, check.ww:929 case-match path — NOT the subset arm). A
* symmetric regression pin; the A7 fix must not perturb it. */
{ "spread_named_casematch",
"package main;\n"
"type e1 = (bool | int);\n"
"type sp = (str | ...e1);\n"
"fn f(x: sp) int = {\n"
" match (x) {\n"
" case let s: str => { return 1; };\n"
" case let e: e1 => { return 2; };\n"
" };\n"
"};\n"
"export fn main() int = { return 0; };\n",
0, 0, 0 },
/* (5b) spread → wider subset assign (latent) → KEEP lenient accept.
* sp's src side carries a `...e1` spread → the guard keeps the
* lenient escape (do NOT newly reject); cstage flattens & accepts.
* byte-id NOT asserted (#199b spread divergence). */
{ "spread_wider_subset",
"package main;\n"
"type e1 = (bool | int);\n"
"type sp = (str | ...e1);\n"
"type big = (str | bool | int | f64);\n"
"fn g(x: sp) big = { return x; };\n"
"export fn main() int = { return 0; };\n",
1, 0, 0 },
};
/* run_build — build+run `src` via `driver`; returns the binary's exit
* code, or -1 on a build failure. */
static int
run_build(const char *driver, const struct row *r, int i)
{
char src[64], tmpdir[64], cmd[1024];
snprintf(src, sizeof src, "/tmp/tsr_%d_%d.ww", getpid(), i);
snprintf(tmpdir, sizeof tmpdir, "/tmp/tsr_%d_d_%d", getpid(), i);
FILE *f = fopen(src, "wb");
if (!f) return -2;
fputs(r->src, f);
fclose(f);
mkdir(tmpdir, 0755);
snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>/dev/null",
tmpdir, driver, src);
int brc = runwait(cmd);
const char *base = strrchr(src, '/');
base = base ? base + 1 : src;
char outbin[128];
snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base);
char *dot = strrchr(outbin, '.');
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
int got = -1;
if (brc == 0) got = runwait(outbin);
unlink(src); unlink(outbin); rmdir(tmpdir);
return brc == 0 ? got : -1;
}
/* build_should_fail — the build must error on `driver`; returns 0 when it
* correctly FAILS, non-zero when it wrongly succeeded. */
static int
build_should_fail(const char *driver, const char *src, int i)
{
char s[64], tmpdir[64], cmd[1024];
snprintf(s, sizeof s, "/tmp/tsrn_%d_%d.ww", getpid(), i);
snprintf(tmpdir, sizeof tmpdir, "/tmp/tsrn_%d_d_%d", getpid(), i);
FILE *f = fopen(s, "wb");
if (!f) return -1;
fputs(src, f);
fclose(f);
mkdir(tmpdir, 0755);
snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>/dev/null",
tmpdir, driver, s);
int rc = runwait(cmd);
unlink(s);
const char *base = strrchr(s, '/');
base = base ? base + 1 : s;
char outbin[128];
snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base);
char *dot = strrchr(outbin, '.');
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
unlink(outbin);
rmdir(tmpdir);
return rc == 0 ? -1 : 0; /* build must NOT succeed */
}
/* asm_byte_identical — w6c vs w6c_ww .s for the same source must match. */
static int
asm_byte_identical(const char *bin, const struct row *r, int i)
{
char src[64], cs[64], ws[64], cmd[1024];
snprintf(src, sizeof src, "/tmp/tsr_asm_%d_%d.ww", getpid(), i);
snprintf(cs, sizeof cs, "/tmp/tsr_asm_%d_%d_c.s", getpid(), i);
snprintf(ws, sizeof ws, "/tmp/tsr_asm_%d_%d_w.s", getpid(), i);
FILE *f = fopen(src, "wb");
if (!f) return -1;
fputs(r->src, f);
fclose(f);
snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s 2>/dev/null", bin, cs, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: w6c errored\n", r->label);
unlink(src);
return -1;
}
snprintf(cmd, sizeof cmd, "%s/w6c_ww -o %s %s 2>/dev/null",
bin, ws, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: w6c_ww errored\n", r->label);
unlink(src); unlink(cs);
return -1;
}
FILE *fc = fopen(cs, "rb");
FILE *fw = fopen(ws, "rb");
int rc = 0;
if (!fc || !fw) {
rc = -1;
} else {
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);
if (rc != 0)
fprintf(stderr, "row[%s]: cstage vs wwstage asm differs\n",
r->label);
unlink(src); unlink(cs); unlink(ws);
return rc;
}
/* Cross-module rows — the leaf-bridge teeth. A bare inline-union variant
* (callee) vs its qualified consumer spelling is matched ONLY by the leaf
* bridge, and that mix is intrinsically multi-module (one file → all names
* bare → typeeqast's streq matches). 839-style `-I` build; byte-id NOT
* asserted (cross-module tagged return + re-pass crosses a pre-existing
* cgen frame-layout divergence, cf. 839). */
struct xrow {
const char *label;
const char *esrc; /* package e */
const char *msrc; /* package main; import e */
int expect_build; /* 1 = build+run to want_exit; 0 = must FAIL */
int want_exit;
};
static const struct xrow xrows[] = {
/* leaf-bridge ACCEPT: e.next returns the inline union (i64|done)
* spelled BARE; forward into qualified (i64|e.done). typeeqast/
* aliassym cannot resolve the bare `done` in main's scope, so only
* the leaf bridge equates them. Dropping the bridge → cs=0/ww=1. */
{ "xmod_inline_forward",
"package e;\n"
"export type done = !i64;\n"
"export fn next() (i64 | done) = { return 0; };\n",
"package main;\n"
"import e;\n"
"type myres = (i64 | e.done);\n"
"fn forward() myres = { return e.next(); };\n"
"export fn main() i32 = { return 0; };\n",
1, 0 },
/* named genuine-absence REJECT: src carries `more`, absent from dst
* (i64|e.done) → the subset loop rejects a NAMED variant, both
* stages. Guards the over-accept the lenient escape used to allow. */
{ "xmod_inline_narrow",
"package e;\n"
"export type done = !i64;\n"
"export type more = !i64;\n"
"export fn next() (i64 | done | more) = { return 0; };\n",
"package main;\n"
"import e;\n"
"type myres = (i64 | e.done);\n"
"fn forward() myres = { return e.next(); };\n"
"export fn main() i32 = { return 0; };\n",
0, 0 },
};
/* run_xmod — write e.ww + main.ww into a fresh dir, `ww build -I dir
* main.ww`, run dir/main. Returns the binary's exit, or -1 on build
* failure (matches the run_build/build_should_fail convention). */
static int
run_xmod(const char *driver, const struct xrow *x)
{
char dir[] = "/tmp/tsrx_XXXXXX";
if (mkdtemp(dir) == NULL) return -2;
char path[1024], cmd[4096];
int wrote = 0;
snprintf(path, sizeof path, "%s/e.ww", dir);
FILE *fe = fopen(path, "wb");
if (fe) { fputs(x->esrc, fe); fclose(fe); wrote++; }
snprintf(path, sizeof path, "%s/main.ww", dir);
FILE *fm = fopen(path, "wb");
if (fm) { fputs(x->msrc, fm); fclose(fm); wrote++; }
if (wrote != 2) {
snprintf(cmd, sizeof cmd, "rm -rf %s", dir);
(void)runwait(cmd);
return -2;
}
snprintf(cmd, sizeof cmd, "cd %s && %s build -I %s %s/main.ww "
"2>/dev/null", dir, driver, dir, dir);
int brc = runwait(cmd);
int got = -1;
if (brc == 0) {
snprintf(path, sizeof path, "%s/main", dir);
got = runwait(path);
}
snprintf(cmd, sizeof cmd, "rm -rf %s", dir);
(void)runwait(cmd);
return brc == 0 ? got : -1;
}
int
main(void)
{
const char *bin = getenv("BIN");
if (!bin) bin = "out/bin";
char absbin[1024];
if (bin[0] != '/') {
char cwd[1024];
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
bin = absbin;
}
char cdrv[1024], wdrv[1024];
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin);
struct { const char *name; const char *path; int gated; }
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
&& access(drivers[d].path, X_OK) != 0) {
fprintf(stderr, "tagged_subset_reject: skip %s (no %s)\n",
drivers[d].name, drivers[d].path);
continue;
}
for (int i = 0; i < n; i++) {
total++;
if (rows[i].expect_build) {
int got = run_build(drivers[d].path, &rows[i], i);
if (got != rows[i].want_exit) {
fprintf(stderr,
"tagged_subset_reject[%s][%s]: "
"exit=%d want=%d\n",
drivers[d].name, rows[i].label,
got, rows[i].want_exit);
fail++;
}
} else {
if (build_should_fail(drivers[d].path,
rows[i].src, 100 + i) != 0) {
fprintf(stderr,
"tagged_subset_reject[%s][%s]: "
"built ok, expected a loud reject\n",
drivers[d].name, rows[i].label);
fail++;
}
}
}
int xn = (int)(sizeof xrows / sizeof xrows[0]);
for (int i = 0; i < xn; i++) {
total++;
int got = run_xmod(drivers[d].path, &xrows[i]);
if (xrows[i].expect_build) {
if (got != xrows[i].want_exit) {
fprintf(stderr,
"tagged_subset_reject[%s][%s]: "
"exit=%d want=%d\n",
drivers[d].name, xrows[i].label,
got, xrows[i].want_exit);
fail++;
}
} else if (got != -1) {
fprintf(stderr,
"tagged_subset_reject[%s][%s]: "
"built ok, expected a loud reject\n",
drivers[d].name, xrows[i].label);
fail++;
}
}
}
/* byte-id rows: w6c vs w6c_ww .s must match (rule-10). */
if (access(wdrv, X_OK) == 0) {
for (int i = 0; i < n; i++) {
if (!rows[i].byteid) continue;
total++;
if (asm_byte_identical(bin, &rows[i], i) != 0)
fail++;
}
}
if (fail) {
fprintf(stderr,
"tagged_subset_reject: %d/%d fixtures failed\n",
fail, total);
return 1;
}
printf("tagged_subset_reject: %d/%d ok\n", total, total);
return 0;
}