cgen: #95 c2 structural variant fallback — both-stage fused

A nominally-unrelated, structurally-equal NAMED source into a NAMED
variant (kb95_unrel: ta/tb same-layout structs, src ta -> (void|tb))
was LIVE both-wrong-identical byte-id silent: both checkers accept,
both cgens tagged 0. After c1's chain arm finds no shared chain
node, match the variant whose CHASED type type_eq's/typeeq's the
source's chased bottom — chased type EQUALITY only, no
type_is_assignable scalar import, no int widening (ken's binding
scalar warning). Same NAMED-source branch, both stages
(cg_tag_for_variant / flatvariantidxt), forced fuse.

Correctness reference, cite 1 — harec tagged_select_subtype P2+P3
(ref/harec/src/types.c:702-739), verbatim:

	if (t->id == subtype->id) {
		return t;
	}
	if (type_is_assignable(ctx, t, subtype)) {
		selected = t;
		++nassign;
	}
	...
	if (nassign == 1) {
		return selected;
	}
	return NULL;

with type_is_assignable's non-tagged path dealiasing both sides and
accepting composites only via interned pointer equality
(types.c:988-1002), verbatim:

	if (type_dealias(ctx, to)->storage != STORAGE_TAGGED) {
		to = type_dealias(ctx, to);
		from = type_dealias(ctx, from);
	}
	...
	if (to == from && to->storage != STORAGE_VOID) {
		return true;
	}

Cite 2 — type_hash interns bare composites STRUCTURALLY (banked as
types.c:72-81; verified in the vendored copy at types.c:444 +
struct/union arm :514-525), verbatim:

	case STORAGE_UNION:
		hash = fnv1a_size(hash, type->struct_union.packed);
		for (const struct struct_field *field = type->struct_union.fields;
				field; field = field->next) {
			if (field->name) {
				hash = fnv1a_s(hash, field->name);
			}
			hash = fnv1a_u32(hash, type_hash(field->type));
			hash = fnv1a_size(hash, field->offset);
		}

— no decl ident in the hash, so harec's two decls dealias to ONE
interned node and `to == from` holds: acceptance is DEFINITIONAL
under interning, not an arm whose text could be misread. Our store
does not intern; chased type equality is the non-interned rendering
of the same rule.

Honest divergence (the >=2-structural-match hard-error STAYS): under
harec's interning two structurally-identical variants are ONE type —
a union cannot contain it twice — so the ambiguity case is
unrepresentable there; our hard-error (twin texts, shared tail
"source structurally matches >=2 variants — ambiguous without
nominal layout (#95)") is the correct nominal-lossy-model rendering,
not a harec deviation.

Pin table: unrel_struct row added (kb95_unrel graduates ok/1-ok/1 ->
0/0, byte-id held) — suite now 48/48. All c1 rows unmoved.

Invariants: 163-row dissolution matrix at tip — same 3 family
graduations as c1, ZERO new movers; five mains cs-vs-ww byte-id OK;
make all 0; sizelint 0; peellint 0 (no new peel sites — the
structural leg reads only chased ends); all 944 suites + 808 green.
w6c_ww/wwdump_ww main.combined.ww regen'd.
This commit is contained in:
2026-06-06 07:43:39 +09:00
parent 34c86bd681
commit 56aac85f6f
5 changed files with 134 additions and 0 deletions

View File

@@ -359,6 +359,20 @@ static const struct row rows[] = {
" return 0;\n"
"};\n", 0, 0, K_RUN_CS_WWERR,
"deferred #277" }, /* ww: task #277 */
/* ---- c2: structural fallback — was both-wrong-identical
* byte-id silent (live metric-1: checkers accept, cgen tagged
* 0). harec accepts by interning (one node, tag tb). */
{ "unrel_struct",
"package main;\n"
"type ta = struct { a: int, b: int };\n"
"type tb = struct { a: int, b: int };\n"
"export fn main() i32 = {\n"
" let s: ta;\n"
" s.a = 4; s.b = 9;\n"
" let v: (void | tb) = s;\n"
" if (!(v is tb)) { return 1; };\n"
" return 0;\n"
"};\n", 0, 0, K_RUN, NULL },
};
static int