diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 0261a91a..5dc5d646 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -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"); diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 448aecf7..56f8aff7 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -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 || diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index 82328af7..2c1c31b4 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -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"); diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index bf9b343e..1384dab4 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -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); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index a4f7c9fa..1e7a7b9b 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -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");