wcc: collapse exprfloatkind structural body onto checker stamp (#121)

wwstage exprfloatkind now reads n.type_ via classifytinfo (typeisf32→1,
typeisfloat→2, else 0) — wwstage classifies float-ness from the cstage-
equal stamp; delete the 9-arm structural body + the transient bridge.

Permanent residual sibling-evidence guards: cgbin float-arith (primary,
fires on unstamped float operand), tupstore + cgwidentaggedstore float
arms (pins, unreachable today, contract assertions against future
regression). cgcast excluded (int→float CVTSI2SD legitimately nil-typed).

Closes the gate-blind cs≠ww float family root (STATUS:14, named (b)-phase
goal). Builds on 98e1665 (A-narrow N_IDENT destructure stamp) +
1c4cea4 (transient floatness-disagreement bridge).
This commit is contained in:
2026-05-26 18:30:18 +09:00
parent 1c4cea4e2c
commit 0a2747ce5b
5 changed files with 387 additions and 570 deletions

View File

@@ -12777,201 +12777,39 @@ 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 = {
// exprfloatkind — classify an expression's value-class for cgen's
// float-vs-integer dispatch. Returns 0 integer-like/unknown, 1 f32,
// 2 f64. Reads the checker-stamped type (n.type_) directly — the
// single source of truth shared with cstage (cmd/wcc/cgen.c
// node_isfloat / type_isf32). Cite typ.ww:437,350 for the TY_NAMED-
// peeling float predicates; rule 13 routes through the type table.
//
// #121 (Package B) — the COLLAPSE. The wwstage previously mirrored
// each cgen-shape arm structurally (N_FLOATLIT/N_INTLIT/N_CAST/N_INDEX/
// N_IDENT/N_UN/N_BIN/N_CALL/N_DOT — fanned out across isf32type,
// localfindnode, c.lets, fnretlookupmod): a gate-blind divergence net
// surfaced by the residual sibling-evidence guards below now that the
// stamp is the SSoT. The TRANSIENT bridge that proved this safe (commits
// 1c4cea4 + 1865+ this file at HEAD-1) is deleted; the corpus-wide
// no-float-miss evidence from commit 2's make-test-green licenses the
// flip + delete. Stamp coverage prereqs landed at 98e1665 (N_IDENT-
// callee destructure stamp) and historically across A.6.x.
//
// PRECONDITION (enforced by the residual sibling-evidence guards at
// cgbin float-arith + cgwidentaggedstore float arm — see those sites):
// any operand whose float-ness drives downstream MOVSD/MOVSS/CVT* MUST
// carry a non-nil n.type_ stamp. Unstamped float nodes (a future
// `let (frac, exp) = math.frexpf64(x);` once N_DOT-callee destructure
// stamping lands — #16/#17) would silently misclassify integer here
// and break in the consumer; the guards turn that into a loud abort.
export fn exprfloatkind(c: *cgen, n: *node) i32 = {
if (n == nil) { return 0; };
let t: *tinfo = n.type_: *tinfo;
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/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. 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; };
if (k == nkind.N_INTLIT) {
// A float-typed integer literal (`8f64`/`0f32`) now
// materialises in X0 (#103 FACE X), so the cast / spill /
// arith-recursion sides must classify it as float — else
// `(8f64 * 10.0): i32` recurses to this N_INTLIT lhs and
// falls through to integer, emitting MOVSXD not CVTTSD2SI
// (the #101 structural-vs-stamped asymmetry). cstage reads
// the checker-stamped type via node_isfloat directly.
if (isf32type(c, n)) { return 1; };
if (isfloattype(c, n)) { return 2; };
return 0;
};
if (k == nkind.N_CAST) {
if (isf32type(c, n.rhs)) { return 1; };
if (isfloattype(c, n.rhs)) { return 2; };
return 0;
};
if (k == nkind.N_INDEX) {
// #119: a float array/slice element feeds cgbin / cgcast through
// X0 (the #119 load is MOVSS/MOVSD into X0). Without this arm the
// wwstage consumer falls to integer (PUSHQ/ADDQ, MOVSXD) while
// the cstage reads the stamped operand type and uses ADDSD/
// CVTTSD2SI — a rule-10 divergence the #119 load fix exposes (the
// consumer was never aligned for indexed float operands). The
// index-result type_ IS checker-stamped (cgindex reads it for
// esz at the N_DOT/N_INDEX-base arms), so this is NOT the
// unstamped-N_MLET-base trap that deferred the broader collapse
// (#121) — only the always-stamped N_INDEX case is classified.
if (isf32type(c, n)) { return 1; };
if (isfloattype(c, n)) { return 2; };
return 0;
};
if (k == nkind.N_IDENT) {
let lc: *local = localfindnode(c, n.str);
if (lc != nil) {
if (isf32type(c, lc.tnode)) { return 1; };
if (isfloattype(c, lc.tnode)) { return 2; };
return 0;
};
let lv: *letvar = c.lets;
for (lv != nil) {
if (streq(lv.name, n.str)) {
if (isf32type(c, lv.tnode)) { return 1; };
if (isfloattype(c, lv.tnode)) { return 2; };
return 0;
};
lv = lv.lvnext;
};
return 0;
};
if (k == nkind.N_UN) {
// Unary on a float (TK_MINUS) returns float; everything
// else is integer-coded.
if (n.op == tkind.TK_MINUS) {
return exprfloatkind(c, n.lhs);
};
return 0;
};
if (k == nkind.N_BIN) {
// Arithmetic binops inherit the operands' kind. Comparison
// (eq/ne/lt/...) returns bool — integer.
let op: tkind = n.op;
if (op == tkind.TK_PLUS) { return exprfloatkind(c, n.lhs); };
if (op == tkind.TK_MINUS) { return exprfloatkind(c, n.lhs); };
if (op == tkind.TK_STAR) { return exprfloatkind(c, n.lhs); };
if (op == tkind.TK_SLASH) { return exprfloatkind(c, n.lhs); };
return 0;
};
if (k == nkind.N_CALL) {
// Look up the callee's declared return type — fnretlookupmod
// returns the type-AST. Routes float-returning fns through
// the X0 ABI so cglet / cgassign know to spill from X0.
// N_DOT (cross-module callee, #98/#101): without the explicit
// arm a module-qualified `myf.g()` callee never reaches any
// lookup, so an imported f64-returning fn fell through to
// integer (0) — cgcast then emitted MOVSXD not CVTTSD2SI
// (#101) and pushargsrev spilled the result as a GPR not
// MOVSD (#98). Mirror nodeisslice / nodeisstr's #34 N_DOT arm
// so cross-module resolves to kind 2 like same-module does.
let callee: *node = n.lhs;
if (callee != nil) {
if (callee.kind == nkind.N_IDENT) {
let rtyp: *node = fnretlookupmod(c, callee.str, c.curmod);
if (isf32type(c, rtyp)) { return 1; };
if (isfloattype(c, rtyp)) { return 2; };
};
if (callee.kind == nkind.N_DOT) {
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
let rtyp: *node = fnretlookupmod(c, callee.str, cmod);
if (isf32type(c, rtyp)) { return 1; };
if (isfloattype(c, rtyp)) { return 2; };
};
};
return 0;
};
if (k == nkind.N_DOT) {
// `p.field` where the struct field is f64/f32. Without this,
// `v.fval: i64` lowers to CVTSI on an integer-load value
// instead of CVTTSD2SI on the X0 the cgdot path actually
// emits for an f64 field. Read the checker-stamped tinfo on
// the N_DOT itself (check.ww:1973 stamps the field type);
// cstage cgen.c:2128 reads node_isfloat(n) the same way.
// Collapsed per A.6.3g (#55).
if (isf32type(c, n)) { return 1; };
if (isfloattype(c, n)) { return 2; };
return 0;
};
return 0;
};
// isnullabletype — `(*T | void)` one-word fold per Hare's
// `(*T | null)` semantics. Cite cstage cgen.c:396 `type_isnullable`;
// the .nullable flag lands on tinfo at check.ww:1309-1318 when the
@@ -13737,6 +13575,31 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s
// str-shape fallback that picks tag 0 for an `(i64 | f64)` union.
let fkind: i32 = exprfloatkind(c, src);
if (fkind != 0) {
// #121 (Package B) RESIDUAL sibling-evidence guard, pin form.
// Post-collapse exprfloatkind reads src.type_; fkind!=0 here
// implies src.type_!=nil — making this assertion structurally
// unreachable today. It is RETAINED on purpose to PIN the
// contract: "the float arm requires a stamped value." If a
// future change replaces exprfloatkind with a non-stamp-driven
// classifier (or adds a float-store dispatch path that doesn't
// pre-read the stamp), this guard catches the regression. Loud-
// abort idiom mirrors cgbin's twin above + cgenstmt.ww:1405/
// 1475 + asserttyped file:line at check.ww:3340-3344.
if (src != nil) { if (src.type_ == nil) {
let msg: str = "cgwidentaggedstore float-arm: src unstamped (#121 sibling-evidence) at ";
os.write(2, msg.ptr, msg.len: u64);
if (src.file.len > 0) {
os.write(2, src.file.ptr, src.file.len: u64);
os.write(2, ":".ptr, 1u64);
let ls: str = strconv.i32tos(src.line, strconv.base.DEC);
os.write(2, ls.ptr, ls.len: u64);
os.write(2, " ".ptr, 1u64);
};
let kn: str = nkname(src.kind);
os.write(2, kn.ptr, kn.len: u64);
os.write(2, "\n".ptr, 1u64);
os.exit(1);
}; };
let fmov: str = "MOVSD";
if (fkind == 1) { fmov = "MOVSS"; };
cgexpr(c, src);
@@ -17235,6 +17098,55 @@ fn cgbin(c: *cgen, n: *node) void = {
let fk: i32 = lfk;
if (fk == 0) { fk = rfk; };
if (fk != 0) {
// #121 (Package B) RESIDUAL sibling-evidence guard. The
// exprfloatkind collapse reads n.type_ as the SSoT; an
// UNSTAMPED operand (type_==nil) misclassifies as 0 and would
// silently take the integer arm of a float binop — exactly the
// gate-blind miscompile the structural oracle used to mask
// (and the bridge proved corpus-clean for at 1c4cea4). With
// the structural net gone, a future N_DOT-callee float-tuple
// destructure (`let (frac, exp) = math.frexpf64(x);` —
// blocked on #16/#17 N_DOT-callee stamp) would leave its
// bindings unstamped → fall through here without this assert.
// Predicate: operand whose own efk==0 (not classified as
// float) AND type_==nil (UNSTAMPED, not a stamped non-float
// like an int passed through a deliberate path). Sibling
// evidence: the binop is float (fk!=0), so the operand should
// either be float (efk!=0) or a stamped non-float (e.g. int);
// nil-typed is the dangerous case. cgcast (cgenexpr.ww:417) is
// EXCLUDED — int→float source is legitimately a CVTSI2SD
// target. Loud-abort idiom mirrors cgenstmt.ww:1405/1475 +
// asserttyped file:line at check.ww:3340-3344.
if (lfk == 0) { if (n.lhs != nil) { if (n.lhs.type_ == nil) {
let msg: str = "cgbin float-arith: lhs operand unstamped (#121 sibling-evidence) at ";
os.write(2, msg.ptr, msg.len: u64);
if (n.lhs.file.len > 0) {
os.write(2, n.lhs.file.ptr, n.lhs.file.len: u64);
os.write(2, ":".ptr, 1u64);
let ls: str = strconv.i32tos(n.lhs.line, strconv.base.DEC);
os.write(2, ls.ptr, ls.len: u64);
os.write(2, " ".ptr, 1u64);
};
let kn: str = nkname(n.lhs.kind);
os.write(2, kn.ptr, kn.len: u64);
os.write(2, "\n".ptr, 1u64);
os.exit(1);
}; }; };
if (rfk == 0) { if (n.rhs != nil) { if (n.rhs.type_ == nil) {
let msg: str = "cgbin float-arith: rhs operand unstamped (#121 sibling-evidence) at ";
os.write(2, msg.ptr, msg.len: u64);
if (n.rhs.file.len > 0) {
os.write(2, n.rhs.file.ptr, n.rhs.file.len: u64);
os.write(2, ":".ptr, 1u64);
let ls: str = strconv.i32tos(n.rhs.line, strconv.base.DEC);
os.write(2, ls.ptr, ls.len: u64);
os.write(2, " ".ptr, 1u64);
};
let kn: str = nkname(n.rhs.kind);
os.write(2, kn.ptr, kn.len: u64);
os.write(2, "\n".ptr, 1u64);
os.exit(1);
}; }; };
let mov: str = "MOVSD";
if (fk == 1) { mov = "MOVSS"; };
if (n.op == tkind.TK_PLUS ||
@@ -20791,6 +20703,33 @@ fn tupstore(c: *cgen, cur: i32, off: i32, wide: bool, tn: *node) void = {
// the FACE-Z field read sees it. X0 survives the reg->mem stores.
// Single-float scope; multi-float collides on X0 at RETURN (#107).
if (isfloattype(c, tn)) {
// #121 (Package B) RESIDUAL sibling-evidence guard, pin form.
// In destructure mode tn IS the tuple-element-type-AST node
// (commit 98e1665's N_MLET arm sets l.lhs = pt.lhs); the "value
// stored" rides X0 with no separate AST. isfloattype(c, tn) at
// the branch head already implies tn.type_!=nil (typeisfloat is
// false on nil), so this assertion is structurally unreachable
// today — RETAINED to PIN the contract: "the float-store branch
// requires a stamped slot." Catches a future change that opens
// this branch on a nil-typed tn (e.g. an N_DOT-callee float-tuple
// element binding where the destructure stamp didn't land —
// #16/#17 cascade). Loud-abort idiom mirrors cgenstmt.ww:1405/
// 1475 + asserttyped file:line at check.ww:3340-3344.
if (tn != nil) { if (tn.type_ == nil) {
let msg: str = "tupstore float-arm: slot tn unstamped (#121 sibling-evidence) at ";
os.write(2, msg.ptr, msg.len: u64);
if (tn.file.len > 0) {
os.write(2, tn.file.ptr, tn.file.len: u64);
os.write(2, ":".ptr, 1u64);
let ls: str = strconv.i32tos(tn.line, strconv.base.DEC);
os.write(2, ls.ptr, ls.len: u64);
os.write(2, " ".ptr, 1u64);
};
let kn: str = nkname(tn.kind);
os.write(2, kn.ptr, kn.len: u64);
os.write(2, "\n".ptr, 1u64);
os.exit(1);
}; };
let mov: str = "MOVSD";
if (isf32type(c, tn)) { mov = "MOVSS"; };
emitline("\t");

View File

@@ -2884,6 +2884,55 @@ fn cgbin(c: *cgen, n: *node) void = {
let fk: i32 = lfk;
if (fk == 0) { fk = rfk; };
if (fk != 0) {
// #121 (Package B) RESIDUAL sibling-evidence guard. The
// exprfloatkind collapse reads n.type_ as the SSoT; an
// UNSTAMPED operand (type_==nil) misclassifies as 0 and would
// silently take the integer arm of a float binop — exactly the
// gate-blind miscompile the structural oracle used to mask
// (and the bridge proved corpus-clean for at 1c4cea4). With
// the structural net gone, a future N_DOT-callee float-tuple
// destructure (`let (frac, exp) = math.frexpf64(x);` —
// blocked on #16/#17 N_DOT-callee stamp) would leave its
// bindings unstamped → fall through here without this assert.
// Predicate: operand whose own efk==0 (not classified as
// float) AND type_==nil (UNSTAMPED, not a stamped non-float
// like an int passed through a deliberate path). Sibling
// evidence: the binop is float (fk!=0), so the operand should
// either be float (efk!=0) or a stamped non-float (e.g. int);
// nil-typed is the dangerous case. cgcast (cgenexpr.ww:417) is
// EXCLUDED — int→float source is legitimately a CVTSI2SD
// target. Loud-abort idiom mirrors cgenstmt.ww:1405/1475 +
// asserttyped file:line at check.ww:3340-3344.
if (lfk == 0) { if (n.lhs != nil) { if (n.lhs.type_ == nil) {
let msg: str = "cgbin float-arith: lhs operand unstamped (#121 sibling-evidence) at ";
os.write(2, msg.ptr, msg.len: u64);
if (n.lhs.file.len > 0) {
os.write(2, n.lhs.file.ptr, n.lhs.file.len: u64);
os.write(2, ":".ptr, 1u64);
let ls: str = strconv.i32tos(n.lhs.line, strconv.base.DEC);
os.write(2, ls.ptr, ls.len: u64);
os.write(2, " ".ptr, 1u64);
};
let kn: str = nkname(n.lhs.kind);
os.write(2, kn.ptr, kn.len: u64);
os.write(2, "\n".ptr, 1u64);
os.exit(1);
}; }; };
if (rfk == 0) { if (n.rhs != nil) { if (n.rhs.type_ == nil) {
let msg: str = "cgbin float-arith: rhs operand unstamped (#121 sibling-evidence) at ";
os.write(2, msg.ptr, msg.len: u64);
if (n.rhs.file.len > 0) {
os.write(2, n.rhs.file.ptr, n.rhs.file.len: u64);
os.write(2, ":".ptr, 1u64);
let ls: str = strconv.i32tos(n.rhs.line, strconv.base.DEC);
os.write(2, ls.ptr, ls.len: u64);
os.write(2, " ".ptr, 1u64);
};
let kn: str = nkname(n.rhs.kind);
os.write(2, kn.ptr, kn.len: u64);
os.write(2, "\n".ptr, 1u64);
os.exit(1);
}; }; };
let mov: str = "MOVSD";
if (fk == 1) { mov = "MOVSS"; };
if (n.op == tkind.TK_PLUS ||

View File

@@ -187,6 +187,33 @@ fn tupstore(c: *cgen, cur: i32, off: i32, wide: bool, tn: *node) void = {
// the FACE-Z field read sees it. X0 survives the reg->mem stores.
// Single-float scope; multi-float collides on X0 at RETURN (#107).
if (isfloattype(c, tn)) {
// #121 (Package B) RESIDUAL sibling-evidence guard, pin form.
// In destructure mode tn IS the tuple-element-type-AST node
// (commit 98e1665's N_MLET arm sets l.lhs = pt.lhs); the "value
// stored" rides X0 with no separate AST. isfloattype(c, tn) at
// the branch head already implies tn.type_!=nil (typeisfloat is
// false on nil), so this assertion is structurally unreachable
// today — RETAINED to PIN the contract: "the float-store branch
// requires a stamped slot." Catches a future change that opens
// this branch on a nil-typed tn (e.g. an N_DOT-callee float-tuple
// element binding where the destructure stamp didn't land —
// #16/#17 cascade). Loud-abort idiom mirrors cgenstmt.ww:1405/
// 1475 + asserttyped file:line at check.ww:3340-3344.
if (tn != nil) { if (tn.type_ == nil) {
let msg: str = "tupstore float-arm: slot tn unstamped (#121 sibling-evidence) at ";
os.write(2, msg.ptr, msg.len: u64);
if (tn.file.len > 0) {
os.write(2, tn.file.ptr, tn.file.len: u64);
os.write(2, ":".ptr, 1u64);
let ls: str = strconv.i32tos(tn.line, strconv.base.DEC);
os.write(2, ls.ptr, ls.len: u64);
os.write(2, " ".ptr, 1u64);
};
let kn: str = nkname(tn.kind);
os.write(2, kn.ptr, kn.len: u64);
os.write(2, "\n".ptr, 1u64);
os.exit(1);
}; };
let mov: str = "MOVSD";
if (isf32type(c, tn)) { mov = "MOVSS"; };
emitline("\t");

View File

@@ -1860,201 +1860,39 @@ 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 = {
// exprfloatkind — classify an expression's value-class for cgen's
// float-vs-integer dispatch. Returns 0 integer-like/unknown, 1 f32,
// 2 f64. Reads the checker-stamped type (n.type_) directly — the
// single source of truth shared with cstage (cmd/wcc/cgen.c
// node_isfloat / type_isf32). Cite typ.ww:437,350 for the TY_NAMED-
// peeling float predicates; rule 13 routes through the type table.
//
// #121 (Package B) — the COLLAPSE. The wwstage previously mirrored
// each cgen-shape arm structurally (N_FLOATLIT/N_INTLIT/N_CAST/N_INDEX/
// N_IDENT/N_UN/N_BIN/N_CALL/N_DOT — fanned out across isf32type,
// localfindnode, c.lets, fnretlookupmod): a gate-blind divergence net
// surfaced by the residual sibling-evidence guards below now that the
// stamp is the SSoT. The TRANSIENT bridge that proved this safe (commits
// 1c4cea4 + 1865+ this file at HEAD-1) is deleted; the corpus-wide
// no-float-miss evidence from commit 2's make-test-green licenses the
// flip + delete. Stamp coverage prereqs landed at 98e1665 (N_IDENT-
// callee destructure stamp) and historically across A.6.x.
//
// PRECONDITION (enforced by the residual sibling-evidence guards at
// cgbin float-arith + cgwidentaggedstore float arm — see those sites):
// any operand whose float-ness drives downstream MOVSD/MOVSS/CVT* MUST
// carry a non-nil n.type_ stamp. Unstamped float nodes (a future
// `let (frac, exp) = math.frexpf64(x);` once N_DOT-callee destructure
// stamping lands — #16/#17) would silently misclassify integer here
// and break in the consumer; the guards turn that into a loud abort.
export fn exprfloatkind(c: *cgen, n: *node) i32 = {
if (n == nil) { return 0; };
let t: *tinfo = n.type_: *tinfo;
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/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. 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; };
if (k == nkind.N_INTLIT) {
// A float-typed integer literal (`8f64`/`0f32`) now
// materialises in X0 (#103 FACE X), so the cast / spill /
// arith-recursion sides must classify it as float — else
// `(8f64 * 10.0): i32` recurses to this N_INTLIT lhs and
// falls through to integer, emitting MOVSXD not CVTTSD2SI
// (the #101 structural-vs-stamped asymmetry). cstage reads
// the checker-stamped type via node_isfloat directly.
if (isf32type(c, n)) { return 1; };
if (isfloattype(c, n)) { return 2; };
return 0;
};
if (k == nkind.N_CAST) {
if (isf32type(c, n.rhs)) { return 1; };
if (isfloattype(c, n.rhs)) { return 2; };
return 0;
};
if (k == nkind.N_INDEX) {
// #119: a float array/slice element feeds cgbin / cgcast through
// X0 (the #119 load is MOVSS/MOVSD into X0). Without this arm the
// wwstage consumer falls to integer (PUSHQ/ADDQ, MOVSXD) while
// the cstage reads the stamped operand type and uses ADDSD/
// CVTTSD2SI — a rule-10 divergence the #119 load fix exposes (the
// consumer was never aligned for indexed float operands). The
// index-result type_ IS checker-stamped (cgindex reads it for
// esz at the N_DOT/N_INDEX-base arms), so this is NOT the
// unstamped-N_MLET-base trap that deferred the broader collapse
// (#121) — only the always-stamped N_INDEX case is classified.
if (isf32type(c, n)) { return 1; };
if (isfloattype(c, n)) { return 2; };
return 0;
};
if (k == nkind.N_IDENT) {
let lc: *local = localfindnode(c, n.str);
if (lc != nil) {
if (isf32type(c, lc.tnode)) { return 1; };
if (isfloattype(c, lc.tnode)) { return 2; };
return 0;
};
let lv: *letvar = c.lets;
for (lv != nil) {
if (streq(lv.name, n.str)) {
if (isf32type(c, lv.tnode)) { return 1; };
if (isfloattype(c, lv.tnode)) { return 2; };
return 0;
};
lv = lv.lvnext;
};
return 0;
};
if (k == nkind.N_UN) {
// Unary on a float (TK_MINUS) returns float; everything
// else is integer-coded.
if (n.op == tkind.TK_MINUS) {
return exprfloatkind(c, n.lhs);
};
return 0;
};
if (k == nkind.N_BIN) {
// Arithmetic binops inherit the operands' kind. Comparison
// (eq/ne/lt/...) returns bool — integer.
let op: tkind = n.op;
if (op == tkind.TK_PLUS) { return exprfloatkind(c, n.lhs); };
if (op == tkind.TK_MINUS) { return exprfloatkind(c, n.lhs); };
if (op == tkind.TK_STAR) { return exprfloatkind(c, n.lhs); };
if (op == tkind.TK_SLASH) { return exprfloatkind(c, n.lhs); };
return 0;
};
if (k == nkind.N_CALL) {
// Look up the callee's declared return type — fnretlookupmod
// returns the type-AST. Routes float-returning fns through
// the X0 ABI so cglet / cgassign know to spill from X0.
// N_DOT (cross-module callee, #98/#101): without the explicit
// arm a module-qualified `myf.g()` callee never reaches any
// lookup, so an imported f64-returning fn fell through to
// integer (0) — cgcast then emitted MOVSXD not CVTTSD2SI
// (#101) and pushargsrev spilled the result as a GPR not
// MOVSD (#98). Mirror nodeisslice / nodeisstr's #34 N_DOT arm
// so cross-module resolves to kind 2 like same-module does.
let callee: *node = n.lhs;
if (callee != nil) {
if (callee.kind == nkind.N_IDENT) {
let rtyp: *node = fnretlookupmod(c, callee.str, c.curmod);
if (isf32type(c, rtyp)) { return 1; };
if (isfloattype(c, rtyp)) { return 2; };
};
if (callee.kind == nkind.N_DOT) {
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
let rtyp: *node = fnretlookupmod(c, callee.str, cmod);
if (isf32type(c, rtyp)) { return 1; };
if (isfloattype(c, rtyp)) { return 2; };
};
};
return 0;
};
if (k == nkind.N_DOT) {
// `p.field` where the struct field is f64/f32. Without this,
// `v.fval: i64` lowers to CVTSI on an integer-load value
// instead of CVTTSD2SI on the X0 the cgdot path actually
// emits for an f64 field. Read the checker-stamped tinfo on
// the N_DOT itself (check.ww:1973 stamps the field type);
// cstage cgen.c:2128 reads node_isfloat(n) the same way.
// Collapsed per A.6.3g (#55).
if (isf32type(c, n)) { return 1; };
if (isfloattype(c, n)) { return 2; };
return 0;
};
return 0;
};
// isnullabletype — `(*T | void)` one-word fold per Hare's
// `(*T | null)` semantics. Cite cstage cgen.c:396 `type_isnullable`;
// the .nullable flag lands on tinfo at check.ww:1309-1318 when the
@@ -2820,6 +2658,31 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s
// str-shape fallback that picks tag 0 for an `(i64 | f64)` union.
let fkind: i32 = exprfloatkind(c, src);
if (fkind != 0) {
// #121 (Package B) RESIDUAL sibling-evidence guard, pin form.
// Post-collapse exprfloatkind reads src.type_; fkind!=0 here
// implies src.type_!=nil — making this assertion structurally
// unreachable today. It is RETAINED on purpose to PIN the
// contract: "the float arm requires a stamped value." If a
// future change replaces exprfloatkind with a non-stamp-driven
// classifier (or adds a float-store dispatch path that doesn't
// pre-read the stamp), this guard catches the regression. Loud-
// abort idiom mirrors cgbin's twin above + cgenstmt.ww:1405/
// 1475 + asserttyped file:line at check.ww:3340-3344.
if (src != nil) { if (src.type_ == nil) {
let msg: str = "cgwidentaggedstore float-arm: src unstamped (#121 sibling-evidence) at ";
os.write(2, msg.ptr, msg.len: u64);
if (src.file.len > 0) {
os.write(2, src.file.ptr, src.file.len: u64);
os.write(2, ":".ptr, 1u64);
let ls: str = strconv.i32tos(src.line, strconv.base.DEC);
os.write(2, ls.ptr, ls.len: u64);
os.write(2, " ".ptr, 1u64);
};
let kn: str = nkname(src.kind);
os.write(2, kn.ptr, kn.len: u64);
os.write(2, "\n".ptr, 1u64);
os.exit(1);
}; };
let fmov: str = "MOVSD";
if (fkind == 1) { fmov = "MOVSS"; };
cgexpr(c, src);

View File

@@ -12777,201 +12777,39 @@ 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 = {
// exprfloatkind — classify an expression's value-class for cgen's
// float-vs-integer dispatch. Returns 0 integer-like/unknown, 1 f32,
// 2 f64. Reads the checker-stamped type (n.type_) directly — the
// single source of truth shared with cstage (cmd/wcc/cgen.c
// node_isfloat / type_isf32). Cite typ.ww:437,350 for the TY_NAMED-
// peeling float predicates; rule 13 routes through the type table.
//
// #121 (Package B) — the COLLAPSE. The wwstage previously mirrored
// each cgen-shape arm structurally (N_FLOATLIT/N_INTLIT/N_CAST/N_INDEX/
// N_IDENT/N_UN/N_BIN/N_CALL/N_DOT — fanned out across isf32type,
// localfindnode, c.lets, fnretlookupmod): a gate-blind divergence net
// surfaced by the residual sibling-evidence guards below now that the
// stamp is the SSoT. The TRANSIENT bridge that proved this safe (commits
// 1c4cea4 + 1865+ this file at HEAD-1) is deleted; the corpus-wide
// no-float-miss evidence from commit 2's make-test-green licenses the
// flip + delete. Stamp coverage prereqs landed at 98e1665 (N_IDENT-
// callee destructure stamp) and historically across A.6.x.
//
// PRECONDITION (enforced by the residual sibling-evidence guards at
// cgbin float-arith + cgwidentaggedstore float arm — see those sites):
// any operand whose float-ness drives downstream MOVSD/MOVSS/CVT* MUST
// carry a non-nil n.type_ stamp. Unstamped float nodes (a future
// `let (frac, exp) = math.frexpf64(x);` once N_DOT-callee destructure
// stamping lands — #16/#17) would silently misclassify integer here
// and break in the consumer; the guards turn that into a loud abort.
export fn exprfloatkind(c: *cgen, n: *node) i32 = {
if (n == nil) { return 0; };
let t: *tinfo = n.type_: *tinfo;
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/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. 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; };
if (k == nkind.N_INTLIT) {
// A float-typed integer literal (`8f64`/`0f32`) now
// materialises in X0 (#103 FACE X), so the cast / spill /
// arith-recursion sides must classify it as float — else
// `(8f64 * 10.0): i32` recurses to this N_INTLIT lhs and
// falls through to integer, emitting MOVSXD not CVTTSD2SI
// (the #101 structural-vs-stamped asymmetry). cstage reads
// the checker-stamped type via node_isfloat directly.
if (isf32type(c, n)) { return 1; };
if (isfloattype(c, n)) { return 2; };
return 0;
};
if (k == nkind.N_CAST) {
if (isf32type(c, n.rhs)) { return 1; };
if (isfloattype(c, n.rhs)) { return 2; };
return 0;
};
if (k == nkind.N_INDEX) {
// #119: a float array/slice element feeds cgbin / cgcast through
// X0 (the #119 load is MOVSS/MOVSD into X0). Without this arm the
// wwstage consumer falls to integer (PUSHQ/ADDQ, MOVSXD) while
// the cstage reads the stamped operand type and uses ADDSD/
// CVTTSD2SI — a rule-10 divergence the #119 load fix exposes (the
// consumer was never aligned for indexed float operands). The
// index-result type_ IS checker-stamped (cgindex reads it for
// esz at the N_DOT/N_INDEX-base arms), so this is NOT the
// unstamped-N_MLET-base trap that deferred the broader collapse
// (#121) — only the always-stamped N_INDEX case is classified.
if (isf32type(c, n)) { return 1; };
if (isfloattype(c, n)) { return 2; };
return 0;
};
if (k == nkind.N_IDENT) {
let lc: *local = localfindnode(c, n.str);
if (lc != nil) {
if (isf32type(c, lc.tnode)) { return 1; };
if (isfloattype(c, lc.tnode)) { return 2; };
return 0;
};
let lv: *letvar = c.lets;
for (lv != nil) {
if (streq(lv.name, n.str)) {
if (isf32type(c, lv.tnode)) { return 1; };
if (isfloattype(c, lv.tnode)) { return 2; };
return 0;
};
lv = lv.lvnext;
};
return 0;
};
if (k == nkind.N_UN) {
// Unary on a float (TK_MINUS) returns float; everything
// else is integer-coded.
if (n.op == tkind.TK_MINUS) {
return exprfloatkind(c, n.lhs);
};
return 0;
};
if (k == nkind.N_BIN) {
// Arithmetic binops inherit the operands' kind. Comparison
// (eq/ne/lt/...) returns bool — integer.
let op: tkind = n.op;
if (op == tkind.TK_PLUS) { return exprfloatkind(c, n.lhs); };
if (op == tkind.TK_MINUS) { return exprfloatkind(c, n.lhs); };
if (op == tkind.TK_STAR) { return exprfloatkind(c, n.lhs); };
if (op == tkind.TK_SLASH) { return exprfloatkind(c, n.lhs); };
return 0;
};
if (k == nkind.N_CALL) {
// Look up the callee's declared return type — fnretlookupmod
// returns the type-AST. Routes float-returning fns through
// the X0 ABI so cglet / cgassign know to spill from X0.
// N_DOT (cross-module callee, #98/#101): without the explicit
// arm a module-qualified `myf.g()` callee never reaches any
// lookup, so an imported f64-returning fn fell through to
// integer (0) — cgcast then emitted MOVSXD not CVTTSD2SI
// (#101) and pushargsrev spilled the result as a GPR not
// MOVSD (#98). Mirror nodeisslice / nodeisstr's #34 N_DOT arm
// so cross-module resolves to kind 2 like same-module does.
let callee: *node = n.lhs;
if (callee != nil) {
if (callee.kind == nkind.N_IDENT) {
let rtyp: *node = fnretlookupmod(c, callee.str, c.curmod);
if (isf32type(c, rtyp)) { return 1; };
if (isfloattype(c, rtyp)) { return 2; };
};
if (callee.kind == nkind.N_DOT) {
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
let rtyp: *node = fnretlookupmod(c, callee.str, cmod);
if (isf32type(c, rtyp)) { return 1; };
if (isfloattype(c, rtyp)) { return 2; };
};
};
return 0;
};
if (k == nkind.N_DOT) {
// `p.field` where the struct field is f64/f32. Without this,
// `v.fval: i64` lowers to CVTSI on an integer-load value
// instead of CVTTSD2SI on the X0 the cgdot path actually
// emits for an f64 field. Read the checker-stamped tinfo on
// the N_DOT itself (check.ww:1973 stamps the field type);
// cstage cgen.c:2128 reads node_isfloat(n) the same way.
// Collapsed per A.6.3g (#55).
if (isf32type(c, n)) { return 1; };
if (isfloattype(c, n)) { return 2; };
return 0;
};
return 0;
};
// isnullabletype — `(*T | void)` one-word fold per Hare's
// `(*T | null)` semantics. Cite cstage cgen.c:396 `type_isnullable`;
// the .nullable flag lands on tinfo at check.ww:1309-1318 when the
@@ -13737,6 +13575,31 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s
// str-shape fallback that picks tag 0 for an `(i64 | f64)` union.
let fkind: i32 = exprfloatkind(c, src);
if (fkind != 0) {
// #121 (Package B) RESIDUAL sibling-evidence guard, pin form.
// Post-collapse exprfloatkind reads src.type_; fkind!=0 here
// implies src.type_!=nil — making this assertion structurally
// unreachable today. It is RETAINED on purpose to PIN the
// contract: "the float arm requires a stamped value." If a
// future change replaces exprfloatkind with a non-stamp-driven
// classifier (or adds a float-store dispatch path that doesn't
// pre-read the stamp), this guard catches the regression. Loud-
// abort idiom mirrors cgbin's twin above + cgenstmt.ww:1405/
// 1475 + asserttyped file:line at check.ww:3340-3344.
if (src != nil) { if (src.type_ == nil) {
let msg: str = "cgwidentaggedstore float-arm: src unstamped (#121 sibling-evidence) at ";
os.write(2, msg.ptr, msg.len: u64);
if (src.file.len > 0) {
os.write(2, src.file.ptr, src.file.len: u64);
os.write(2, ":".ptr, 1u64);
let ls: str = strconv.i32tos(src.line, strconv.base.DEC);
os.write(2, ls.ptr, ls.len: u64);
os.write(2, " ".ptr, 1u64);
};
let kn: str = nkname(src.kind);
os.write(2, kn.ptr, kn.len: u64);
os.write(2, "\n".ptr, 1u64);
os.exit(1);
}; };
let fmov: str = "MOVSD";
if (fkind == 1) { fmov = "MOVSS"; };
cgexpr(c, src);
@@ -17235,6 +17098,55 @@ fn cgbin(c: *cgen, n: *node) void = {
let fk: i32 = lfk;
if (fk == 0) { fk = rfk; };
if (fk != 0) {
// #121 (Package B) RESIDUAL sibling-evidence guard. The
// exprfloatkind collapse reads n.type_ as the SSoT; an
// UNSTAMPED operand (type_==nil) misclassifies as 0 and would
// silently take the integer arm of a float binop — exactly the
// gate-blind miscompile the structural oracle used to mask
// (and the bridge proved corpus-clean for at 1c4cea4). With
// the structural net gone, a future N_DOT-callee float-tuple
// destructure (`let (frac, exp) = math.frexpf64(x);` —
// blocked on #16/#17 N_DOT-callee stamp) would leave its
// bindings unstamped → fall through here without this assert.
// Predicate: operand whose own efk==0 (not classified as
// float) AND type_==nil (UNSTAMPED, not a stamped non-float
// like an int passed through a deliberate path). Sibling
// evidence: the binop is float (fk!=0), so the operand should
// either be float (efk!=0) or a stamped non-float (e.g. int);
// nil-typed is the dangerous case. cgcast (cgenexpr.ww:417) is
// EXCLUDED — int→float source is legitimately a CVTSI2SD
// target. Loud-abort idiom mirrors cgenstmt.ww:1405/1475 +
// asserttyped file:line at check.ww:3340-3344.
if (lfk == 0) { if (n.lhs != nil) { if (n.lhs.type_ == nil) {
let msg: str = "cgbin float-arith: lhs operand unstamped (#121 sibling-evidence) at ";
os.write(2, msg.ptr, msg.len: u64);
if (n.lhs.file.len > 0) {
os.write(2, n.lhs.file.ptr, n.lhs.file.len: u64);
os.write(2, ":".ptr, 1u64);
let ls: str = strconv.i32tos(n.lhs.line, strconv.base.DEC);
os.write(2, ls.ptr, ls.len: u64);
os.write(2, " ".ptr, 1u64);
};
let kn: str = nkname(n.lhs.kind);
os.write(2, kn.ptr, kn.len: u64);
os.write(2, "\n".ptr, 1u64);
os.exit(1);
}; }; };
if (rfk == 0) { if (n.rhs != nil) { if (n.rhs.type_ == nil) {
let msg: str = "cgbin float-arith: rhs operand unstamped (#121 sibling-evidence) at ";
os.write(2, msg.ptr, msg.len: u64);
if (n.rhs.file.len > 0) {
os.write(2, n.rhs.file.ptr, n.rhs.file.len: u64);
os.write(2, ":".ptr, 1u64);
let ls: str = strconv.i32tos(n.rhs.line, strconv.base.DEC);
os.write(2, ls.ptr, ls.len: u64);
os.write(2, " ".ptr, 1u64);
};
let kn: str = nkname(n.rhs.kind);
os.write(2, kn.ptr, kn.len: u64);
os.write(2, "\n".ptr, 1u64);
os.exit(1);
}; }; };
let mov: str = "MOVSD";
if (fk == 1) { mov = "MOVSS"; };
if (n.op == tkind.TK_PLUS ||
@@ -20791,6 +20703,33 @@ fn tupstore(c: *cgen, cur: i32, off: i32, wide: bool, tn: *node) void = {
// the FACE-Z field read sees it. X0 survives the reg->mem stores.
// Single-float scope; multi-float collides on X0 at RETURN (#107).
if (isfloattype(c, tn)) {
// #121 (Package B) RESIDUAL sibling-evidence guard, pin form.
// In destructure mode tn IS the tuple-element-type-AST node
// (commit 98e1665's N_MLET arm sets l.lhs = pt.lhs); the "value
// stored" rides X0 with no separate AST. isfloattype(c, tn) at
// the branch head already implies tn.type_!=nil (typeisfloat is
// false on nil), so this assertion is structurally unreachable
// today — RETAINED to PIN the contract: "the float-store branch
// requires a stamped slot." Catches a future change that opens
// this branch on a nil-typed tn (e.g. an N_DOT-callee float-tuple
// element binding where the destructure stamp didn't land —
// #16/#17 cascade). Loud-abort idiom mirrors cgenstmt.ww:1405/
// 1475 + asserttyped file:line at check.ww:3340-3344.
if (tn != nil) { if (tn.type_ == nil) {
let msg: str = "tupstore float-arm: slot tn unstamped (#121 sibling-evidence) at ";
os.write(2, msg.ptr, msg.len: u64);
if (tn.file.len > 0) {
os.write(2, tn.file.ptr, tn.file.len: u64);
os.write(2, ":".ptr, 1u64);
let ls: str = strconv.i32tos(tn.line, strconv.base.DEC);
os.write(2, ls.ptr, ls.len: u64);
os.write(2, " ".ptr, 1u64);
};
let kn: str = nkname(tn.kind);
os.write(2, kn.ptr, kn.len: u64);
os.write(2, "\n".ptr, 1u64);
os.exit(1);
}; };
let mov: str = "MOVSD";
if (isf32type(c, tn)) { mov = "MOVSS"; };
emitline("\t");