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

@@ -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