Commit Graph

3 Commits

Author SHA1 Message Date
fc50a27f3e cgen: #95 c3 reviewer-fold — is/as gate exact-only, no widening leak
c1/c2 widened flatvariantidxt (selfhost) with the chain + structural
tag-synthesis arms and a >=2 ambiguity os.exit, scoped to the cgen
WIDEN consumer. But flatvariantidxt is a choke-point: the wwstage is/as
ACCEPTANCE gate (check.ww:4677, the #198 spread fallback) reuses it, so
the widening leaked into checker acceptance — vs base 329481c:
  * `let v:(void|ali)=…; v is base` (ali=base): cstage rejects, wwstage
    ACCEPTED+built — new cs!=ww acceptance divergence (rule-10 break);
  * `(void|tb)`, `v is ta` (unrelated same-layout): same leak via the c2
    structural arm;
  * `(ali|ali2)`, `v is base`: wwstage DIED with the cgen-internal fatal
    "flatvariantidxt: source alias chain reaches >=2 variants" DURING
    CHECK — a cgen diag surfacing in the checker (layering).
cstage is unaffected: its is/as gate (check.c:2036) is independent of
cg_tag_for_variant (cgen-phase only), so the fuse was already broken at
this site — the cgen-helper change moved wwstage's CHECKER but not
cstage's. This contradicts the #95 fold scope ("cgen-tag fold, no
acceptance change except the ambiguity hard-error [at the widen site]").

Fix (rob-ruled): the is/as gate needs only nominal variant membership =
pass 1. Add an explicit `exactonly` mode to flatvariantidxt — the
checker caller passes true (returns after the exact loop: no chain/
structural arms, no os.exit), every cgen caller passes false (full
tag-synthesis, unchanged). Two consumers, two modes — the honest
representation, not a wrapper. cstage's cg_tag_for_variant has no twin
checker caller, so it stays full-only and is UNTOUCHED by c3 (rule-10
satisfied: the param changes no asm — cgen always passes false; the
checker now MATCHES cstage's reject). casevariantin still backs the
#198 spread fallback.

Pins (test/wcc/944_variant_chain_b95_run.c, +4 rows -> 56 checks):
  isas_chain_reject / isas_unrel_reject — BOTH stages reject the leaked
  is/as shapes (shared experr substring "not a variant"); the c1 chain +
  c2 structural arms no longer widen acceptance.
  isas_amb_reject_notcrash — the (ali|ali2)/`is base` shape rejects
  CLEANLY (the cgen fatal text would be absent -> red), NOT a crash.
  twin_prim_alias_amb — rob's obligated mixed prim/alias TWIN:
  (int | ai) ai=int, source aj=int — both share the int bottom under
  all-variants counting, so the cgen WIDEN (full mode) hard-errors
  ("source alias chain reaches >=2 variants"), pinned LOUD both stages.

The deferred question (should is/as EVER accept cgen's richer chain/
structural shapes? = a checker-strictness feature, both stages together)
is filed as task #107, explicitly NOT folded here.

Invariants: c1/c2 cgen behavior unchanged (all cgen callers pass false =
full mode); suite byte-id rows + the dissolution corpus hold. make all
0; sizelint 0; peellint 0 (the mode param adds no peel sites); combined.ww
regen idempotent; test-unit "all 295 tests passed". c3 touches ZERO
cstage bytes — cmd/w6c/cgen.c carries only the c1/c2 additions, and
cmd/wcc/check.c is unchanged from base 329481c.
2026-06-06 08:07:03 +09:00
56aac85f6f 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.
2026-06-06 07:43:39 +09:00
34c86bd681 cgen: #95 c1 chain-membership variant arm — both-stage fused
A NAMED struct source that was not pointer-identical to a NAMED
variant fell through every pass of cg_tag_for_variant (cmd/w6c/
cgen.c) / flatvariantidxt (selfhost/cmd/wcc/cgenutil.ww) and the
widen stored tag 0 — both stages, byte-identical, gate-blind: wrong
tag on VALID code at any alias depth, in both chain directions
(.ai/ken-95-oracle.md §2: kb5_v2s1i, kb95_2lvl_i, kb95_deep_src,
kb95_deep_var all both-wrong-identical at base).

New pass 1b, identical both stages (the same route — forced fuse):
after pass-1 exact (unchanged, FIRST — the (str|linerr) protection,
harec's P1 short-circuit), a NAMED source matches the variant whose
NAMED chain shares a pointer-identical node with the source's chain
(an alias IS-A its base through the chain). Two linear NAMED chains
intersect iff they share their chased bottom node (ken §1), so the
walk is implemented as pointer identity of the chased ends through
type_chase_named/tichase — the blessed chase choke-point. NO raw
.under/->under hops were added, so the anticipated `peel-ok: nominal
chain walk (#95)` annotations are unnecessary and the peellint
whitelist is UNCHANGED (continues the B6/B7 fold-peels-into-chase
arc; peellint green).

Variants are counted UNGATED (bare prims are type-table singletons,
so a bare variant node can BE the source's chased bottom): the >=2
guard stays equivalent to harec's nassign>=2 -> NULL
(ref/harec/src/types.c:734-738, tagged_select_subtype P2/P3). >=2
chain hits hard-error with twin texts (prefix convention, shared
tail "source alias chain reaches >=2 variants — ambiguous without
nominal layout (#95)") — drew's ambiguity proviso extended to the
chained set; was a SILENT member-0 tag. Pass-2 bare-source fallback
unchanged. Chased type EQUALITY only — no type_is_assignable scalar
import, no int widening (ken's binding scalar warning).

Pin table (new suite test/wcc/944_variant_chain_b95_run.c, 45
checks, Makefile-wired):
  GRADUATIONS exit 1->0 both stages: chain_1lvl_i (kb5_v2s1i
  HEADLINE, byte-id held), chain_2lvl_i, chain_deep_src,
  chain_deep_var (byte-id held), chain_call_bound81 (kb5_v2s1),
  chain_call2_bound81 (kb4_v2_struct2, #95's original) — the two
  CALL-src rows waive byte-id, pre-existing #81 zero-fill asm noise
  (NO at base too).
  NEW LOUD: chain_amb_loud (kb95_amb) — silent tag 0 -> hard-error
  both stages.
  MUST-NOT-MOVE held: chain_amb_srcA/B (pass-1 precedence),
  nom_str/nom_err (#218 nominal regression pin), exact_ctl
  (kb5_v2sE2), bare_ctl/bare_2lvl/bare_ambig/bare_ambig2 (pass-2
  controls), callret_bound277 (kb5_v2sE #277 cells unchanged,
  dual-cell pin).

Invariants: ken's 163-row dissolution matrix rerun — exactly 3
movers, all family graduations (v2s1i/v2s1/v2_struct2 1->0), zero
non-family movers, detectors unmoved. Five mains cs-vs-ww byte-id
OK (ww/w6c/w6a/w6l/wwdump). make all 0; sizelint 0; peellint 0; all
944 suites + 808 green. w6c_ww/wwdump_ww main.combined.ww regen'd
(cgenutil.ww embeds).
2026-06-06 07:40:32 +09:00