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

@@ -921,6 +921,33 @@ cg_tag_for_variant(Type *t, Type *vt)
fatal("cg_tag_for_variant: source alias chain "
"reaches >=2 variants — ambiguous without "
"nominal layout (#95)");
if (found >= 0) return found;
/* c2 (#95): no chain hit (found/n are still -1/0 here) —
* structural fallback on the chased ends. harec interns
* bare composites structurally (type_hash, ref/harec/src/
* types.c:72-81), so a nominally-unrelated structurally-
* equal decl DEALIASES TO THE SAME NODE there and the
* assignability arm accepts via `to == from`
* (types.c:1000-1002) — acceptance is definitional, not an
* arm we could misread. Our store does not intern, so the
* pointer compare of pass 1b misses it; chased type_eq is
* the non-interned rendering of the same rule. EQUALITY
* only — no type_is_assignable scalar import. The >=2
* hard-error is the nominal-lossy-model rendering of a case
* harec cannot represent (two structurally-identical
* variants intern to ONE type — a union cannot contain it
* twice), not a harec deviation. */
idx = 0;
for (Tparam *p = t->params; p; p = p->next, idx++) {
if (p->type && type_eq(type_chase_named(p->type), sb)) {
if (found < 0) found = idx;
n++;
}
}
if (n >= 2)
fatal("cg_tag_for_variant: source structurally "
"matches >=2 variants — ambiguous without "
"nominal layout (#95)");
return found;
}
/* Pass 2 (#15): no exact variant matched — try a structural match of

View File

@@ -19021,6 +19021,37 @@ fn flatvariantidxt(tagged: *tinfo, want: *tinfo) i32 = {
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
if (found >= 0) { return found; };
// c2 (#95): no chain hit (found/n are still -1/0 here) —
// structural fallback on the chased ends. harec interns bare
// composites structurally (type_hash, ref/harec/src/
// types.c:72-81), so a nominally-unrelated structurally-
// equal decl DEALIASES TO THE SAME NODE there and the
// assignability arm accepts via `to == from`
// (types.c:1000-1002) — acceptance is definitional, not an
// arm we could misread. Our store does not intern, so the
// pointer compare of pass 1b misses it; chased typeeq is the
// non-interned rendering of the same rule. EQUALITY only —
// no type_is_assignable scalar import. The >=2 hard-error is
// the nominal-lossy-model rendering of a case harec cannot
// represent (two structurally-identical variants intern to
// ONE type — a union cannot contain it twice), not a harec
// deviation. cs twin cg_tag_for_variant fused in this commit.
q = ti.params;
qi = 0;
for (q != nil) {
if (q.type_ != nil && typeeq(tichase(q.type_), sb)) {
if (found < 0) { found = qi; };
n += 1;
};
q = q.tnext;
qi += 1;
};
if (n >= 2) {
let msg2: str = "flatvariantidxt: source structurally matches >=2 variants — ambiguous without nominal layout (#95)\n";
os.write(2, msg2.ptr, msg2.len: u64);
os.exit(1);
};
return found;
};
// Pass 2 (#15): no exact variant matched — structurally match a BARE

View File

@@ -2917,6 +2917,37 @@ fn flatvariantidxt(tagged: *tinfo, want: *tinfo) i32 = {
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
if (found >= 0) { return found; };
// c2 (#95): no chain hit (found/n are still -1/0 here) —
// structural fallback on the chased ends. harec interns bare
// composites structurally (type_hash, ref/harec/src/
// types.c:72-81), so a nominally-unrelated structurally-
// equal decl DEALIASES TO THE SAME NODE there and the
// assignability arm accepts via `to == from`
// (types.c:1000-1002) — acceptance is definitional, not an
// arm we could misread. Our store does not intern, so the
// pointer compare of pass 1b misses it; chased typeeq is the
// non-interned rendering of the same rule. EQUALITY only —
// no type_is_assignable scalar import. The >=2 hard-error is
// the nominal-lossy-model rendering of a case harec cannot
// represent (two structurally-identical variants intern to
// ONE type — a union cannot contain it twice), not a harec
// deviation. cs twin cg_tag_for_variant fused in this commit.
q = ti.params;
qi = 0;
for (q != nil) {
if (q.type_ != nil && typeeq(tichase(q.type_), sb)) {
if (found < 0) { found = qi; };
n += 1;
};
q = q.tnext;
qi += 1;
};
if (n >= 2) {
let msg2: str = "flatvariantidxt: source structurally matches >=2 variants — ambiguous without nominal layout (#95)\n";
os.write(2, msg2.ptr, msg2.len: u64);
os.exit(1);
};
return found;
};
// Pass 2 (#15): no exact variant matched — structurally match a BARE

View File

@@ -19021,6 +19021,37 @@ fn flatvariantidxt(tagged: *tinfo, want: *tinfo) i32 = {
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
if (found >= 0) { return found; };
// c2 (#95): no chain hit (found/n are still -1/0 here) —
// structural fallback on the chased ends. harec interns bare
// composites structurally (type_hash, ref/harec/src/
// types.c:72-81), so a nominally-unrelated structurally-
// equal decl DEALIASES TO THE SAME NODE there and the
// assignability arm accepts via `to == from`
// (types.c:1000-1002) — acceptance is definitional, not an
// arm we could misread. Our store does not intern, so the
// pointer compare of pass 1b misses it; chased typeeq is the
// non-interned rendering of the same rule. EQUALITY only —
// no type_is_assignable scalar import. The >=2 hard-error is
// the nominal-lossy-model rendering of a case harec cannot
// represent (two structurally-identical variants intern to
// ONE type — a union cannot contain it twice), not a harec
// deviation. cs twin cg_tag_for_variant fused in this commit.
q = ti.params;
qi = 0;
for (q != nil) {
if (q.type_ != nil && typeeq(tichase(q.type_), sb)) {
if (found < 0) { found = qi; };
n += 1;
};
q = q.tnext;
qi += 1;
};
if (n >= 2) {
let msg2: str = "flatvariantidxt: source structurally matches >=2 variants — ambiguous without nominal layout (#95)\n";
os.write(2, msg2.ptr, msg2.len: u64);
os.exit(1);
};
return found;
};
// Pass 2 (#15): no exact variant matched — structurally match a BARE

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