wcc: exprfloatkind structural/stamp floatness-disagreement bridge (#121)

Add classifytinfo(n.type_) stampkind alongside structural structkind;
abort on floatness-disagreement only ((structkind!=0)!=(stampkind!=0));
return structkind (byte-id-neutral). TRANSIENT — deleted in commit 3 (the
flip + permanent residual sibling-evidence guard). The corpus-wide
validation that licenses commit 3's flip.
This commit is contained in:
2026-05-26 17:46:39 +09:00
parent 98e166504f
commit 1c4cea4e2c
3 changed files with 210 additions and 21 deletions

View File

@@ -1860,19 +1860,82 @@ export fn isf32type(c: *cgen, t: *node) bool = {
return typeisf32(t.type_: *tinfo);
};
// classifytinfo — map a checker stamp (node.type_ as *tinfo) to the
// exprfloatkind value-class: f32 → 1, f64 → 2, integer/unknown/nil → 0.
// The TY_NAMED peel + TY_F32/F64 split live in typeisf32 / typeisfloat
// (typ.ww:437,350; cf. the split at check.ww:1323) — reuse them rather
// than re-implementing the peel (rule 13: route through the type table).
// #121 (Package B) collapse target: exprtype stamps each node's RESULT
// type on n.type_, so reading it here mirrors every structural arm at
// its actual node. Commit 2 uses it only to CROSS-CHECK the structural
// classifier (the consistency bridge below); commit 3 makes it the body.
fn classifytinfo(t: *tinfo) i32 = {
if (typeisf32(t)) { return 1; };
if (typeisfloat(t)) { return 2; };
return 0;
};
// exprfloatkind — classify an expression's value-class so callers can
// pick float vs integer codegen without a full type system. Returns:
// 0 — integer-like (or unknown — same fallback the existing cgen
// takes today)
// 1 — f32
// 2 — f64
// pick float vs integer codegen without a full type system. Returns
// 0 integer-like/unknown, 1 f32, 2 f64.
//
// #121 (Package B) consistency BRIDGE — byte-id-NEUTRAL, TRANSIENT.
// The wwstage classifies float-ness STRUCTURALLY (exprfloatkindstruct), a
// gate-blind divergence from cstage's checker-stamp read (node_isfloat).
// Before flipping to the stamp (commit 3), this proves the stamp AGREES
// with the structural classifier on every float node the corpus exercises.
// A wrong float classification miscompiles silently and the bootstrap
// barely exercises floats, so the disagreement is surfaced LOUD, not
// absorbed. Whole bridge (struct body + this wrapper) is DELETED in
// commit 3; the residual sibling-evidence guard lands at that point as
// the permanent net.
//
// RELAXED condition — floatness-disagreement ONLY, NOT precision (Drew /
// Rob 2026-05-26):
// abort iff (structkind != 0) != (stampkind != 0).
// CAT A (float↔int): caught loud — a stamp-miss on a float node, or a
// stamp-extra on an int the structural arm correctly classifies as not-
// float; both are real bugs to surface. CAT B (f32-vs-f64 coarseness,
// e.g. structural N_FLOATLIT returns 2 unconditionally while stamp says
// 1 on a tsuffix-stamped 1.5f32): TOLERATED — the stamp is the
// authoritative source (cstage's single source), commit 3's flip
// converges toward it; the structural arm being f32-blind is the very
// reason for the collapse, not a bug to abort on. Non-load-bearing for
// asm in let-store contexts the corpus exercises (964 byte-id holds at
// HEAD with the f32 literals classified coarse f64).
//
// Returns structkind UNCHANGED → behaviour identical to pre-bridge.
export fn exprfloatkind(c: *cgen, n: *node) i32 = {
let structkind: i32 = exprfloatkindstruct(c, n);
let stampkind: i32 = classifytinfo(n.type_: *tinfo);
if ((structkind != 0) != (stampkind != 0)) {
let msg: str = "exprfloatkind: float node mis/un-stamped (cs!=ww) at ";
os.write(2, msg.ptr, msg.len: u64);
if (n.file.len > 0) {
os.write(2, n.file.ptr, n.file.len: u64);
os.write(2, ":".ptr, 1u64);
let ls: str = strconv.i32tos(n.line, strconv.base.DEC);
os.write(2, ls.ptr, ls.len: u64);
os.write(2, " ".ptr, 1u64);
};
let kn: str = nkname(n.kind);
os.write(2, kn.ptr, kn.len: u64);
os.write(2, "\n".ptr, 1u64);
os.exit(1);
};
return structkind;
};
// exprfloatkindstruct — the STRUCTURAL classifier (#121 commit-2 form;
// commit 3 deletes it once the bridge proves the stamp suffices).
// Recognises: float literals, idents bound to float lets/locals,
// chained casts whose target is float, and (recursively) the inner
// expr of a non-narrowing wrapping construct. Anything we can't
// pin down conservatively reports integer — the worst case is that
// CVT* is skipped for an exotic case the user can still spell with
// an explicit local.
export fn exprfloatkind(c: *cgen, n: *node) i32 = {
// an explicit local. Recurses through exprfloatkind (the bridge), so
// the consistency check fires on every sub-node the recursion visits.
fn exprfloatkindstruct(c: *cgen, n: *node) i32 = {
if (n == nil) { return 0; };
let k: nkind = n.kind;
if (k == nkind.N_FLOATLIT) { return 2; };