From d92c199d25b32f087b51be359122ecd384066173 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 28 May 2026 11:35:45 +0900 Subject: [PATCH] wcc: retire exprfloatkind, read the checker stamp directly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit exprfloatkind was wwstage cgen's structural float-classifier — a workaround for the checker stamp being untrustworthy. With the previous commit arming the asserttyped bail, every checked value-node is now stamped (or cited-exempt), so its job collapses to a 2-liner reading n.type_ — the same path cstage cgen has always taken. Retire it: inline the stamp-read at its eight sites (cgcast, cgun, cgbin lhs+rhs, cgcall pop, pushargsrev, cgwidentaggedstorebp, cgreturn x2 collapsed), delete the wrapper, and delete the two residual sibling-evidence loud-aborts (cgbin float-arith, cgwidentaggedstorebp float-arm) — their operands are real source value-exprs the armed bail now stamps, so the guards can never fire. One synth-post-checker value-node remained outside the bail's reach: the variadic-slice descriptor pushed in pushargsrev/cgcall (cgenexpr.ww). Stamp it at synthesis with the variadic param's []T slice tinfo so the inlined reads see a stamped node, no nil special-case. Byte-id-neutral by design (slice tinfo and nil both read non-float); 990-997 confirm. Closes the bail-rearm arc — wwstage now reads the same float-class SSoT cstage does, the gate-blind float-classification family is closed, and the build+test corpus is asserttyped-clean by construction. --- selfhost/cmd/w6c/main.combined.ww | 195 ++++++++++----------------- selfhost/cmd/wcc/cgenexpr.ww | 97 ++++++------- selfhost/cmd/wcc/cgenstmt.ww | 12 +- selfhost/cmd/wcc/cgenutil.ww | 86 +++--------- selfhost/cmd/wwdump/main.combined.ww | 195 ++++++++++----------------- 5 files changed, 222 insertions(+), 363 deletions(-) diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 35f626a8..433c485c 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -14545,7 +14545,12 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { // X0 via SUBQ+MOVSD so cgcall's pop side can drain into the // XMM stream (X0..X7). f32 still occupies 8B on the stack — // the MOVSS load on the pop side touches only the low 4. - let fk: i32 = exprfloatkind(c, arg); + let fk: i32 = 0; + if (arg != nil) { + let at: *tinfo = arg.type_: *tinfo; + if (typeisf32(at)) { fk = 1; } + else { if (typeisfloat(at)) { fk = 2; }; }; + }; if (fk != 0) { cgexpr(c, arg); let mov: str = "MOVSD"; @@ -16137,39 +16142,6 @@ export fn isf32type(c: *cgen, t: *node) bool = { return typeisf32(t.type_: *tinfo); }; -// 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; -}; - // 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 @@ -16928,38 +16900,22 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s // by coincidence (TK_FLOAT lowering loads the f64 bit pattern into AX // before MOVSD'ing into X0); every runtime f64 shape (cast, call, // unary, ident, struct-field load) needs the explicit MOVSD path. - // Mirror of cstage cg_widen_tagged_store's float arm. Wwstage has no - // checker so we classify via exprfloatkind (same shape used by cgcast) - // and resolve the variant tag by name directly — rhstargetname has no - // N_FLOATLIT / N_CALL / N_DOT branch and would fall through to the - // str-shape fallback that picks tag 0 for an `(i64 | f64)` union. - let fkind: i32 = exprfloatkind(c, src); + // Mirror of cstage cg_widen_tagged_store's float arm. Classify off + // the checker stamp (src.type_) — the SSoT cstage reads via + // node_isfloat / type_isf32 — and resolve the variant tag by name + // directly: rhstargetname has no N_FLOATLIT / N_CALL / N_DOT branch + // and would fall through to the str-shape fallback that picks tag 0 + // for an `(i64 | f64)` union. The armed asserttyped bail (check.ww) + // guarantees src carries a non-nil stamp, so the sibling-evidence + // loud-abort that used to pin "the float arm requires a stamped + // value" is dead and removed. + let fkind: i32 = 0; + if (src != nil) { + let srct: *tinfo = src.type_: *tinfo; + if (typeisf32(srct)) { fkind = 1; } + else { if (typeisfloat(srct)) { fkind = 2; }; }; + }; 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); @@ -17988,7 +17944,12 @@ fn cgtypeassert(c: *cgen, n: *node) void = { }; fn cgcast(c: *cgen, n: *node) void = { - let srcfk: i32 = exprfloatkind(c, n.lhs); + let srcfk: i32 = 0; + if (n.lhs != nil) { + let st: *tinfo = n.lhs.type_: *tinfo; + if (typeisf32(st)) { srcfk = 1; } + else { if (typeisfloat(st)) { srcfk = 2; }; }; + }; let dstf64: bool = isfloattype(c, n.rhs); let dstf32: bool = isf32type(c, n.rhs); let dstfk: i32 = 0; @@ -20227,7 +20188,12 @@ fn cgun(c: *cgen, n: *node) void = { // then apply the unary op. AMP / STAR override AX with the // address / deref. The wasted load before AMP keeps our asm // byte-identical to the C version. - let fk: i32 = exprfloatkind(c, n.lhs); + let fk: i32 = 0; + if (n.lhs != nil) { + let lt: *tinfo = n.lhs.type_: *tinfo; + if (typeisf32(lt)) { fk = 1; } + else { if (typeisfloat(lt)) { fk = 2; }; }; + }; if (n.op == tkind.TK_MINUS && fk != 0) { // Float negate: X0 = 0 - X0. Stash orig, load 0.0, subtract. // Zero bit pattern equals 0.0 for both f32 and f64 so we @@ -20693,60 +20659,27 @@ fn cgbin(c: *cgen, n: *node) void = { // general FP register saver. ADDSD/SUBSD/MULSD/DIVSD pick SS // variants for f32. Comparison uses UCOMISD + JCC and falls // out to the existing CMPQ-based path below. - let lfk: i32 = exprfloatkind(c, n.lhs); - let rfk: i32 = exprfloatkind(c, n.rhs); + // Value-class read off the checker stamp (n.type_) — the SSoT + // shared with cstage cgen.c node_isfloat / type_isf32. The armed + // asserttyped bail (check.ww) guarantees every checked value-node + // is stamped, so the read can't see a nil-typed float operand; + // the sibling-evidence loud-aborts that used to pin that contract + // are therefore dead and removed. + let lfk: i32 = 0; + if (n.lhs != nil) { + let llt: *tinfo = n.lhs.type_: *tinfo; + if (typeisf32(llt)) { lfk = 1; } + else { if (typeisfloat(llt)) { lfk = 2; }; }; + }; + let rfk: i32 = 0; + if (n.rhs != nil) { + let rrt: *tinfo = n.rhs.type_: *tinfo; + if (typeisf32(rrt)) { rfk = 1; } + else { if (typeisfloat(rrt)) { rfk = 2; }; }; + }; 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 || @@ -21452,6 +21385,13 @@ fn cgcall(c: *cgen, n: *node) void = { let sn: *node = newnode(nkind.N_IDENT, "", 0, 0); sn.str = sname; + // Synthesised after the checker has run, so the + // asserttyped bail (check.ww) never stamps it. + // Stamp the variadic param's []T slice tinfo + // (resolvefnbody resolve-walks varp.lhs) so the + // downstream value-class reads see a non-nil + // stamp — the one cgen node the bail can't cover. + sn.type_ = varp.lhs.type_; if (prevarg == nil) { n.list = sn; } else { prevarg.next = sn; }; }; @@ -21490,7 +21430,12 @@ fn cgcall(c: *cgen, n: *node) void = { let popped: i32 = 0; let stackslots: i32 = 0; for (a != nil) { - let fk: i32 = exprfloatkind(c, a); + let fk: i32 = 0; + if (a != nil) { + let at: *tinfo = a.type_: *tinfo; + if (typeisf32(at)) { fk = 1; } + else { if (typeisfloat(at)) { fk = 2; }; }; + }; if (fk != 0) { let mov: str = "MOVSD"; if (fk == 1) { mov = "MOVSS"; }; @@ -25051,6 +24996,14 @@ fn cgreturn(c: *cgen, n: *node) void = { // IMULQ before the call) does not land in slot+16 / // slot+24. (Task #18.) let rsz: i32 = slotsize(c, c.fnret); + // Value-class read off the checker stamp (rhs.type_) — + // the SSoT cstage reads via node_isfloat / type_isf32. + let rfk: i32 = 0; + if (rhs != nil) { + let rety: *tinfo = rhs.type_: *tinfo; + if (typeisf32(rety)) { rfk = 1; } + else { if (typeisfloat(rety)) { rfk = 2; }; }; + }; if (nodeisslice(c, rhs)) { // cgexpr leaves (AX=ptr, BX=len, CX=cap). // Shuffle into return ABI: DX=ptr, CX=len, @@ -25065,7 +25018,7 @@ fn cgreturn(c: *cgen, n: *node) void = { emitline("\tMOVQ\tCX, R8\n"); emitline("\tMOVQ\tBX, CX\n"); emitline("\tMOVQ\tAX, DX\n"); - } else { if (exprfloatkind(c, rhs) != 0) { + } else { if (rfk != 0) { // #157: float variant — cgexpr left the value // in X0, not AX. No MOVQ-xmm->gp encoding, so // bridge X0->DX through a stack slot (same arg- @@ -25078,7 +25031,7 @@ fn cgreturn(c: *cgen, n: *node) void = { emitline("\tSUBQ\t$8, SP\n"); emitline("\tMOVQ\t$0, (SP)\n"); let mov: str = "MOVSD"; - if (exprfloatkind(c, rhs) == 1) { mov = "MOVSS"; }; + if (rfk == 1) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\tX0, (SP)\n"); diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index a8c906ff..5df85cb6 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -414,7 +414,12 @@ fn cgtypeassert(c: *cgen, n: *node) void = { }; fn cgcast(c: *cgen, n: *node) void = { - let srcfk: i32 = exprfloatkind(c, n.lhs); + let srcfk: i32 = 0; + if (n.lhs != nil) { + let st: *tinfo = n.lhs.type_: *tinfo; + if (typeisf32(st)) { srcfk = 1; } + else { if (typeisfloat(st)) { srcfk = 2; }; }; + }; let dstf64: bool = isfloattype(c, n.rhs); let dstf32: bool = isf32type(c, n.rhs); let dstfk: i32 = 0; @@ -2653,7 +2658,12 @@ fn cgun(c: *cgen, n: *node) void = { // then apply the unary op. AMP / STAR override AX with the // address / deref. The wasted load before AMP keeps our asm // byte-identical to the C version. - let fk: i32 = exprfloatkind(c, n.lhs); + let fk: i32 = 0; + if (n.lhs != nil) { + let lt: *tinfo = n.lhs.type_: *tinfo; + if (typeisf32(lt)) { fk = 1; } + else { if (typeisfloat(lt)) { fk = 2; }; }; + }; if (n.op == tkind.TK_MINUS && fk != 0) { // Float negate: X0 = 0 - X0. Stash orig, load 0.0, subtract. // Zero bit pattern equals 0.0 for both f32 and f64 so we @@ -3119,60 +3129,27 @@ fn cgbin(c: *cgen, n: *node) void = { // general FP register saver. ADDSD/SUBSD/MULSD/DIVSD pick SS // variants for f32. Comparison uses UCOMISD + JCC and falls // out to the existing CMPQ-based path below. - let lfk: i32 = exprfloatkind(c, n.lhs); - let rfk: i32 = exprfloatkind(c, n.rhs); + // Value-class read off the checker stamp (n.type_) — the SSoT + // shared with cstage cgen.c node_isfloat / type_isf32. The armed + // asserttyped bail (check.ww) guarantees every checked value-node + // is stamped, so the read can't see a nil-typed float operand; + // the sibling-evidence loud-aborts that used to pin that contract + // are therefore dead and removed. + let lfk: i32 = 0; + if (n.lhs != nil) { + let llt: *tinfo = n.lhs.type_: *tinfo; + if (typeisf32(llt)) { lfk = 1; } + else { if (typeisfloat(llt)) { lfk = 2; }; }; + }; + let rfk: i32 = 0; + if (n.rhs != nil) { + let rrt: *tinfo = n.rhs.type_: *tinfo; + if (typeisf32(rrt)) { rfk = 1; } + else { if (typeisfloat(rrt)) { rfk = 2; }; }; + }; 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 || @@ -3878,6 +3855,13 @@ fn cgcall(c: *cgen, n: *node) void = { let sn: *node = newnode(nkind.N_IDENT, "", 0, 0); sn.str = sname; + // Synthesised after the checker has run, so the + // asserttyped bail (check.ww) never stamps it. + // Stamp the variadic param's []T slice tinfo + // (resolvefnbody resolve-walks varp.lhs) so the + // downstream value-class reads see a non-nil + // stamp — the one cgen node the bail can't cover. + sn.type_ = varp.lhs.type_; if (prevarg == nil) { n.list = sn; } else { prevarg.next = sn; }; }; @@ -3916,7 +3900,12 @@ fn cgcall(c: *cgen, n: *node) void = { let popped: i32 = 0; let stackslots: i32 = 0; for (a != nil) { - let fk: i32 = exprfloatkind(c, a); + let fk: i32 = 0; + if (a != nil) { + let at: *tinfo = a.type_: *tinfo; + if (typeisf32(at)) { fk = 1; } + else { if (typeisfloat(at)) { fk = 2; }; }; + }; if (fk != 0) { let mov: str = "MOVSD"; if (fk == 1) { mov = "MOVSS"; }; diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index ecd3184d..1afba74c 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -473,6 +473,14 @@ fn cgreturn(c: *cgen, n: *node) void = { // IMULQ before the call) does not land in slot+16 / // slot+24. (Task #18.) let rsz: i32 = slotsize(c, c.fnret); + // Value-class read off the checker stamp (rhs.type_) — + // the SSoT cstage reads via node_isfloat / type_isf32. + let rfk: i32 = 0; + if (rhs != nil) { + let rety: *tinfo = rhs.type_: *tinfo; + if (typeisf32(rety)) { rfk = 1; } + else { if (typeisfloat(rety)) { rfk = 2; }; }; + }; if (nodeisslice(c, rhs)) { // cgexpr leaves (AX=ptr, BX=len, CX=cap). // Shuffle into return ABI: DX=ptr, CX=len, @@ -487,7 +495,7 @@ fn cgreturn(c: *cgen, n: *node) void = { emitline("\tMOVQ\tCX, R8\n"); emitline("\tMOVQ\tBX, CX\n"); emitline("\tMOVQ\tAX, DX\n"); - } else { if (exprfloatkind(c, rhs) != 0) { + } else { if (rfk != 0) { // #157: float variant — cgexpr left the value // in X0, not AX. No MOVQ-xmm->gp encoding, so // bridge X0->DX through a stack slot (same arg- @@ -500,7 +508,7 @@ fn cgreturn(c: *cgen, n: *node) void = { emitline("\tSUBQ\t$8, SP\n"); emitline("\tMOVQ\t$0, (SP)\n"); let mov: str = "MOVSD"; - if (exprfloatkind(c, rhs) == 1) { mov = "MOVSS"; }; + if (rfk == 1) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\tX0, (SP)\n"); diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 353a22da..379fadf6 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -478,7 +478,12 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { // X0 via SUBQ+MOVSD so cgcall's pop side can drain into the // XMM stream (X0..X7). f32 still occupies 8B on the stack — // the MOVSS load on the pop side touches only the low 4. - let fk: i32 = exprfloatkind(c, arg); + let fk: i32 = 0; + if (arg != nil) { + let at: *tinfo = arg.type_: *tinfo; + if (typeisf32(at)) { fk = 1; } + else { if (typeisfloat(at)) { fk = 2; }; }; + }; if (fk != 0) { cgexpr(c, arg); let mov: str = "MOVSD"; @@ -2070,39 +2075,6 @@ export fn isf32type(c: *cgen, t: *node) bool = { return typeisf32(t.type_: *tinfo); }; -// 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; -}; - // 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 @@ -2861,38 +2833,22 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s // by coincidence (TK_FLOAT lowering loads the f64 bit pattern into AX // before MOVSD'ing into X0); every runtime f64 shape (cast, call, // unary, ident, struct-field load) needs the explicit MOVSD path. - // Mirror of cstage cg_widen_tagged_store's float arm. Wwstage has no - // checker so we classify via exprfloatkind (same shape used by cgcast) - // and resolve the variant tag by name directly — rhstargetname has no - // N_FLOATLIT / N_CALL / N_DOT branch and would fall through to the - // str-shape fallback that picks tag 0 for an `(i64 | f64)` union. - let fkind: i32 = exprfloatkind(c, src); + // Mirror of cstage cg_widen_tagged_store's float arm. Classify off + // the checker stamp (src.type_) — the SSoT cstage reads via + // node_isfloat / type_isf32 — and resolve the variant tag by name + // directly: rhstargetname has no N_FLOATLIT / N_CALL / N_DOT branch + // and would fall through to the str-shape fallback that picks tag 0 + // for an `(i64 | f64)` union. The armed asserttyped bail (check.ww) + // guarantees src carries a non-nil stamp, so the sibling-evidence + // loud-abort that used to pin "the float arm requires a stamped + // value" is dead and removed. + let fkind: i32 = 0; + if (src != nil) { + let srct: *tinfo = src.type_: *tinfo; + if (typeisf32(srct)) { fkind = 1; } + else { if (typeisfloat(srct)) { fkind = 2; }; }; + }; 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 b16c0522..b80449ad 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -14545,7 +14545,12 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { // X0 via SUBQ+MOVSD so cgcall's pop side can drain into the // XMM stream (X0..X7). f32 still occupies 8B on the stack — // the MOVSS load on the pop side touches only the low 4. - let fk: i32 = exprfloatkind(c, arg); + let fk: i32 = 0; + if (arg != nil) { + let at: *tinfo = arg.type_: *tinfo; + if (typeisf32(at)) { fk = 1; } + else { if (typeisfloat(at)) { fk = 2; }; }; + }; if (fk != 0) { cgexpr(c, arg); let mov: str = "MOVSD"; @@ -16137,39 +16142,6 @@ export fn isf32type(c: *cgen, t: *node) bool = { return typeisf32(t.type_: *tinfo); }; -// 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; -}; - // 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 @@ -16928,38 +16900,22 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s // by coincidence (TK_FLOAT lowering loads the f64 bit pattern into AX // before MOVSD'ing into X0); every runtime f64 shape (cast, call, // unary, ident, struct-field load) needs the explicit MOVSD path. - // Mirror of cstage cg_widen_tagged_store's float arm. Wwstage has no - // checker so we classify via exprfloatkind (same shape used by cgcast) - // and resolve the variant tag by name directly — rhstargetname has no - // N_FLOATLIT / N_CALL / N_DOT branch and would fall through to the - // str-shape fallback that picks tag 0 for an `(i64 | f64)` union. - let fkind: i32 = exprfloatkind(c, src); + // Mirror of cstage cg_widen_tagged_store's float arm. Classify off + // the checker stamp (src.type_) — the SSoT cstage reads via + // node_isfloat / type_isf32 — and resolve the variant tag by name + // directly: rhstargetname has no N_FLOATLIT / N_CALL / N_DOT branch + // and would fall through to the str-shape fallback that picks tag 0 + // for an `(i64 | f64)` union. The armed asserttyped bail (check.ww) + // guarantees src carries a non-nil stamp, so the sibling-evidence + // loud-abort that used to pin "the float arm requires a stamped + // value" is dead and removed. + let fkind: i32 = 0; + if (src != nil) { + let srct: *tinfo = src.type_: *tinfo; + if (typeisf32(srct)) { fkind = 1; } + else { if (typeisfloat(srct)) { fkind = 2; }; }; + }; 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); @@ -17988,7 +17944,12 @@ fn cgtypeassert(c: *cgen, n: *node) void = { }; fn cgcast(c: *cgen, n: *node) void = { - let srcfk: i32 = exprfloatkind(c, n.lhs); + let srcfk: i32 = 0; + if (n.lhs != nil) { + let st: *tinfo = n.lhs.type_: *tinfo; + if (typeisf32(st)) { srcfk = 1; } + else { if (typeisfloat(st)) { srcfk = 2; }; }; + }; let dstf64: bool = isfloattype(c, n.rhs); let dstf32: bool = isf32type(c, n.rhs); let dstfk: i32 = 0; @@ -20227,7 +20188,12 @@ fn cgun(c: *cgen, n: *node) void = { // then apply the unary op. AMP / STAR override AX with the // address / deref. The wasted load before AMP keeps our asm // byte-identical to the C version. - let fk: i32 = exprfloatkind(c, n.lhs); + let fk: i32 = 0; + if (n.lhs != nil) { + let lt: *tinfo = n.lhs.type_: *tinfo; + if (typeisf32(lt)) { fk = 1; } + else { if (typeisfloat(lt)) { fk = 2; }; }; + }; if (n.op == tkind.TK_MINUS && fk != 0) { // Float negate: X0 = 0 - X0. Stash orig, load 0.0, subtract. // Zero bit pattern equals 0.0 for both f32 and f64 so we @@ -20693,60 +20659,27 @@ fn cgbin(c: *cgen, n: *node) void = { // general FP register saver. ADDSD/SUBSD/MULSD/DIVSD pick SS // variants for f32. Comparison uses UCOMISD + JCC and falls // out to the existing CMPQ-based path below. - let lfk: i32 = exprfloatkind(c, n.lhs); - let rfk: i32 = exprfloatkind(c, n.rhs); + // Value-class read off the checker stamp (n.type_) — the SSoT + // shared with cstage cgen.c node_isfloat / type_isf32. The armed + // asserttyped bail (check.ww) guarantees every checked value-node + // is stamped, so the read can't see a nil-typed float operand; + // the sibling-evidence loud-aborts that used to pin that contract + // are therefore dead and removed. + let lfk: i32 = 0; + if (n.lhs != nil) { + let llt: *tinfo = n.lhs.type_: *tinfo; + if (typeisf32(llt)) { lfk = 1; } + else { if (typeisfloat(llt)) { lfk = 2; }; }; + }; + let rfk: i32 = 0; + if (n.rhs != nil) { + let rrt: *tinfo = n.rhs.type_: *tinfo; + if (typeisf32(rrt)) { rfk = 1; } + else { if (typeisfloat(rrt)) { rfk = 2; }; }; + }; 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 || @@ -21452,6 +21385,13 @@ fn cgcall(c: *cgen, n: *node) void = { let sn: *node = newnode(nkind.N_IDENT, "", 0, 0); sn.str = sname; + // Synthesised after the checker has run, so the + // asserttyped bail (check.ww) never stamps it. + // Stamp the variadic param's []T slice tinfo + // (resolvefnbody resolve-walks varp.lhs) so the + // downstream value-class reads see a non-nil + // stamp — the one cgen node the bail can't cover. + sn.type_ = varp.lhs.type_; if (prevarg == nil) { n.list = sn; } else { prevarg.next = sn; }; }; @@ -21490,7 +21430,12 @@ fn cgcall(c: *cgen, n: *node) void = { let popped: i32 = 0; let stackslots: i32 = 0; for (a != nil) { - let fk: i32 = exprfloatkind(c, a); + let fk: i32 = 0; + if (a != nil) { + let at: *tinfo = a.type_: *tinfo; + if (typeisf32(at)) { fk = 1; } + else { if (typeisfloat(at)) { fk = 2; }; }; + }; if (fk != 0) { let mov: str = "MOVSD"; if (fk == 1) { mov = "MOVSS"; }; @@ -25051,6 +24996,14 @@ fn cgreturn(c: *cgen, n: *node) void = { // IMULQ before the call) does not land in slot+16 / // slot+24. (Task #18.) let rsz: i32 = slotsize(c, c.fnret); + // Value-class read off the checker stamp (rhs.type_) — + // the SSoT cstage reads via node_isfloat / type_isf32. + let rfk: i32 = 0; + if (rhs != nil) { + let rety: *tinfo = rhs.type_: *tinfo; + if (typeisf32(rety)) { rfk = 1; } + else { if (typeisfloat(rety)) { rfk = 2; }; }; + }; if (nodeisslice(c, rhs)) { // cgexpr leaves (AX=ptr, BX=len, CX=cap). // Shuffle into return ABI: DX=ptr, CX=len, @@ -25065,7 +25018,7 @@ fn cgreturn(c: *cgen, n: *node) void = { emitline("\tMOVQ\tCX, R8\n"); emitline("\tMOVQ\tBX, CX\n"); emitline("\tMOVQ\tAX, DX\n"); - } else { if (exprfloatkind(c, rhs) != 0) { + } else { if (rfk != 0) { // #157: float variant — cgexpr left the value // in X0, not AX. No MOVQ-xmm->gp encoding, so // bridge X0->DX through a stack slot (same arg- @@ -25078,7 +25031,7 @@ fn cgreturn(c: *cgen, n: *node) void = { emitline("\tSUBQ\t$8, SP\n"); emitline("\tMOVQ\t$0, (SP)\n"); let mov: str = "MOVSD"; - if (exprfloatkind(c, rhs) == 1) { mov = "MOVSS"; }; + if (rfk == 1) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\tX0, (SP)\n");