diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 4c242da2..816a4e37 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -12572,6 +12572,18 @@ export fn exprfloatkind(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; }; @@ -12614,18 +12626,35 @@ export fn exprfloatkind(c: *cgen, n: *node) i32 = { return 0; }; if (k == nkind.N_CALL) { - // Look up the callee's declared return type — fnretlookup + // 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. - let nm: str; - nm.ptr = nil; nm.len = 0; - if (n.lhs != nil) { - if (n.lhs.kind == nkind.N_IDENT) { nm = n.lhs.str; }; - }; - if (nm.len > 0) { - let rtyp: *node = fnretlookup(c, nm); - if (isf32type(c, rtyp)) { return 1; }; - if (isfloattype(c, rtyp)) { return 2; }; + // 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; }; @@ -14044,11 +14073,37 @@ import typ; import sym; import strconv; +// cgfloatbits — materialise a float constant in X0: MOVQ the IEEE bits +// into AX, PUSH, MOVSD off the stack into X0. Shared by N_FLOATLIT (bits +// already in n.uval from the lexer's bitcast) and the f64/f32-typed +// N_INTLIT arm (#103 FACE X). +fn cgfloatbits(c: *cgen, bits: u64) void = { + emitline("\tMOVQ\t$"); + emitint(bits: i64); + emitline(", AX\n"); + emitline("\tPUSHQ\tAX\n"); + emitline("\tMOVSD\t(SP), X0\n"); + emitline("\tADDQ\t$8, SP\n"); +}; + fn cgexpr(c: *cgen, n: *node) void = { if (n == nil) { return; }; let k: nkind = n.kind; if (k == nkind.N_INTLIT) { + // A no-decimal `0f64`/`8f64` is an N_INTLIT carrying float + // TYPE; it must reach X0 like a true float literal, not the + // integer-immediate path (which strands it in AX and an SSE + // compare/mul reads a stale X0 — #103 FACE X). The bits are + // the IEEE pattern of the integer value, mirroring cstage's + // `(double)(long long)n->uval`; the (&fv):*u64 bitcast is the + // lex.ww idiom (lib/ww/lex/lex.ww). + if (isfloattype(c, n)) { + let fv: f64 = (n.uval: i64): f64; + let pu: *u64 = (&fv): *u64; + cgfloatbits(c, *pu); + return; + }; // Print signed (i64), not unsigned (u64). C cgen uses // `$%lld` so 64-bit constants with bit 63 set show up as // negative — e.g. FNV-1a's offset basis prints as @@ -14059,19 +14114,11 @@ fn cgexpr(c: *cgen, n: *node) void = { return; }; if (k == nkind.N_FLOATLIT) { - // Materialise the f64 bit pattern in AX, push, then MOVSD it - // into X0. The bits come from n.uval — the parser populates - // it from the lexer's bitcast of t.fval, so this path stays - // integer-only (no SSE in the cgen source). The f32 - // narrowing is handled at the consumer site, not here — the - // literal always carries the full double precision until - // typed by context. - emitline("\tMOVQ\t$"); - emitint(n.uval: i64); - emitline(", AX\n"); - emitline("\tPUSHQ\tAX\n"); - emitline("\tMOVSD\t(SP), X0\n"); - emitline("\tADDQ\t$8, SP\n"); + // The bits come from n.uval — the parser populates it from + // the lexer's bitcast of t.fval. The f32 narrowing is handled + // at the consumer site, not here — the literal always carries + // the full double precision until typed by context. + cgfloatbits(c, n.uval); return; }; if (k == nkind.N_RUNELIT) { @@ -15675,6 +15722,21 @@ fn cgdot(c: *cgen, n: *node) void = { emitline("(BP), CX\n"); return; }; + // f64/f32 tuple field must ride X0 via + // MOVSD/MOVSS; the integer load op left it + // in AX (#103 FACE Z). Mirrors the float + // local load above and cstage cgen.c:1462, + // 1838 (the #96 pattern). + if (isfloattype(c, tpt)) { + let mov: str = "MOVSD"; + if (isf32type(c, tpt)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t"); + emitoff((lc.off + foff): i64); + emitline("(BP), X0\n"); + return; + }; let sz: i32 = slotsize(c, tpt); let op: str = tnodeloadop(c, tpt, sz); emitline("\t"); @@ -20336,7 +20398,7 @@ fn rettupleof(c: *cgen, rhs: *node) *node = { // header (ref/hare/rt/ensure.ha:4-8) at off/+8/+16 from consecutive // cursor registers; a scalar stores 1 word. Byte-identical to the cstage // N_MLET/N_MASSIGN store (cmd/w6c/cgen.c). -fn tupstore(cur: i32, off: i32, wide: bool) void = { +fn tupstore(c: *cgen, cur: i32, off: i32, wide: bool, tn: *node) void = { if (wide) { emitline("\tMOVQ\t"); emitline(tupreg(cur + 0)); @@ -20355,6 +20417,20 @@ fn tupstore(cur: i32, off: i32, wide: bool) void = { emitline("(BP)\n"); return; }; + // #105: an f64/f32 element rides X0 (the SSE return reg), not its + // integer cursor reg — MOVSD/MOVSS it, else the slot gets garbage and + // 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)) { + let mov: str = "MOVSD"; + if (isf32type(c, tn)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\tX0, "); + emitoff(off: i64); + emitline("(BP)\n"); + return; + }; emitline("\tMOVQ\t"); emitline(tupreg(cur)); emitline(", "); @@ -21037,6 +21113,58 @@ fn cglet(c: *cgen, n: *node) void = { }; }; }; + // 16B tuple init from a function call. An integer word rides + // its integer cursor reg (AX, DX); a single f64/f32 word rides + // X0, the SSE return reg — the RETURN leaves the float in X0 + // and pushes garbage through that word's integer slot, so a + // blanket MOVQ-from-integer spill stores garbage and the #103- + // FACE-Z field read (MOVSD-from-slot) reads it (#105). Spill + // each word from its real class. Multi-float tuples collide on + // X0 at the RETURN (#107), out of scope. Mirror of cstage + // cgen.c. Without this branch a 16B tuple receive (any element + // mix) fell to the generic single-word store below and dropped + // word1 — silent loss of t.1 (#102). + let rt16: *node = rettupleof(c, rhs); + if (rt16 != nil && sz == 16) { + let q0: *node = rt16.list; + let q1: *node = nil; + if (q0 != nil) { q1 = q0.next; }; + let q0t: *node = nil; + let q1t: *node = nil; + if (q0 != nil) { q0t = q0.lhs; }; + if (q1 != nil) { q1t = q1.lhs; }; + let e0f: bool = isfloattype(c, q0t); + let e1f: bool = isfloattype(c, q1t); + cgexpr(c, rhs); + if (e0f) { + let mov: str = "MOVSD"; + if (isf32type(c, q0t)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\tX0, "); + emitoff(off: i64); + emitline("(BP)\n"); + } else { + emitline("\tMOVQ\tAX, "); + emitoff(off: i64); + emitline("(BP)\n"); + }; + if (e1f) { + let mov: str = "MOVSD"; + if (isf32type(c, q1t)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\tX0, "); + emitoff((off + 8): i64); + emitline("(BP)\n"); + } else { + emitline("\tMOVQ\tDX, "); + emitoff((off + 8): i64); + emitline("(BP)\n"); + }; + c.lastwasreturn = 0; + return; + }; // Array literal init: `let xs: [N]T = [a, b, c];` (or [_]T). // Walk elements in declaration order, store each at off + i*esz // using the right width for the element type. Trailing `...` @@ -21504,7 +21632,7 @@ fn cgmassign(c: *cgen, n: *node) void = { let off: i32 = 0; if (l.kind == nkind.N_IDENT) { off = localfind(c, l.str); }; if (off != 0) { - tupstore(cur, off, wide); + tupstore(c, cur, off, wide, tn); }; cur = cur + tupebytes(wide); l = l.next; @@ -21576,7 +21704,7 @@ fn cgmlet(c: *cgen, n: *node) void = { let sz: i32 = 8; if (wide) { sz = tyslicesize(): i32; }; let off: i32 = localadd(c, l.str, sz, tn); - tupstore(cur, off, wide); + tupstore(c, cur, off, wide, tn); cur = cur + tupebytes(wide); l = l.next; if (pt != nil) { pt = pt.next; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 9ab1071c..a572cf9d 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -12572,6 +12572,18 @@ export fn exprfloatkind(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; }; @@ -12614,18 +12626,35 @@ export fn exprfloatkind(c: *cgen, n: *node) i32 = { return 0; }; if (k == nkind.N_CALL) { - // Look up the callee's declared return type — fnretlookup + // 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. - let nm: str; - nm.ptr = nil; nm.len = 0; - if (n.lhs != nil) { - if (n.lhs.kind == nkind.N_IDENT) { nm = n.lhs.str; }; - }; - if (nm.len > 0) { - let rtyp: *node = fnretlookup(c, nm); - if (isf32type(c, rtyp)) { return 1; }; - if (isfloattype(c, rtyp)) { return 2; }; + // 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; }; @@ -14044,11 +14073,37 @@ import typ; import sym; import strconv; +// cgfloatbits — materialise a float constant in X0: MOVQ the IEEE bits +// into AX, PUSH, MOVSD off the stack into X0. Shared by N_FLOATLIT (bits +// already in n.uval from the lexer's bitcast) and the f64/f32-typed +// N_INTLIT arm (#103 FACE X). +fn cgfloatbits(c: *cgen, bits: u64) void = { + emitline("\tMOVQ\t$"); + emitint(bits: i64); + emitline(", AX\n"); + emitline("\tPUSHQ\tAX\n"); + emitline("\tMOVSD\t(SP), X0\n"); + emitline("\tADDQ\t$8, SP\n"); +}; + fn cgexpr(c: *cgen, n: *node) void = { if (n == nil) { return; }; let k: nkind = n.kind; if (k == nkind.N_INTLIT) { + // A no-decimal `0f64`/`8f64` is an N_INTLIT carrying float + // TYPE; it must reach X0 like a true float literal, not the + // integer-immediate path (which strands it in AX and an SSE + // compare/mul reads a stale X0 — #103 FACE X). The bits are + // the IEEE pattern of the integer value, mirroring cstage's + // `(double)(long long)n->uval`; the (&fv):*u64 bitcast is the + // lex.ww idiom (lib/ww/lex/lex.ww). + if (isfloattype(c, n)) { + let fv: f64 = (n.uval: i64): f64; + let pu: *u64 = (&fv): *u64; + cgfloatbits(c, *pu); + return; + }; // Print signed (i64), not unsigned (u64). C cgen uses // `$%lld` so 64-bit constants with bit 63 set show up as // negative — e.g. FNV-1a's offset basis prints as @@ -14059,19 +14114,11 @@ fn cgexpr(c: *cgen, n: *node) void = { return; }; if (k == nkind.N_FLOATLIT) { - // Materialise the f64 bit pattern in AX, push, then MOVSD it - // into X0. The bits come from n.uval — the parser populates - // it from the lexer's bitcast of t.fval, so this path stays - // integer-only (no SSE in the cgen source). The f32 - // narrowing is handled at the consumer site, not here — the - // literal always carries the full double precision until - // typed by context. - emitline("\tMOVQ\t$"); - emitint(n.uval: i64); - emitline(", AX\n"); - emitline("\tPUSHQ\tAX\n"); - emitline("\tMOVSD\t(SP), X0\n"); - emitline("\tADDQ\t$8, SP\n"); + // The bits come from n.uval — the parser populates it from + // the lexer's bitcast of t.fval. The f32 narrowing is handled + // at the consumer site, not here — the literal always carries + // the full double precision until typed by context. + cgfloatbits(c, n.uval); return; }; if (k == nkind.N_RUNELIT) { @@ -15675,6 +15722,21 @@ fn cgdot(c: *cgen, n: *node) void = { emitline("(BP), CX\n"); return; }; + // f64/f32 tuple field must ride X0 via + // MOVSD/MOVSS; the integer load op left it + // in AX (#103 FACE Z). Mirrors the float + // local load above and cstage cgen.c:1462, + // 1838 (the #96 pattern). + if (isfloattype(c, tpt)) { + let mov: str = "MOVSD"; + if (isf32type(c, tpt)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t"); + emitoff((lc.off + foff): i64); + emitline("(BP), X0\n"); + return; + }; let sz: i32 = slotsize(c, tpt); let op: str = tnodeloadop(c, tpt, sz); emitline("\t"); @@ -20336,7 +20398,7 @@ fn rettupleof(c: *cgen, rhs: *node) *node = { // header (ref/hare/rt/ensure.ha:4-8) at off/+8/+16 from consecutive // cursor registers; a scalar stores 1 word. Byte-identical to the cstage // N_MLET/N_MASSIGN store (cmd/w6c/cgen.c). -fn tupstore(cur: i32, off: i32, wide: bool) void = { +fn tupstore(c: *cgen, cur: i32, off: i32, wide: bool, tn: *node) void = { if (wide) { emitline("\tMOVQ\t"); emitline(tupreg(cur + 0)); @@ -20355,6 +20417,20 @@ fn tupstore(cur: i32, off: i32, wide: bool) void = { emitline("(BP)\n"); return; }; + // #105: an f64/f32 element rides X0 (the SSE return reg), not its + // integer cursor reg — MOVSD/MOVSS it, else the slot gets garbage and + // 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)) { + let mov: str = "MOVSD"; + if (isf32type(c, tn)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\tX0, "); + emitoff(off: i64); + emitline("(BP)\n"); + return; + }; emitline("\tMOVQ\t"); emitline(tupreg(cur)); emitline(", "); @@ -21037,6 +21113,58 @@ fn cglet(c: *cgen, n: *node) void = { }; }; }; + // 16B tuple init from a function call. An integer word rides + // its integer cursor reg (AX, DX); a single f64/f32 word rides + // X0, the SSE return reg — the RETURN leaves the float in X0 + // and pushes garbage through that word's integer slot, so a + // blanket MOVQ-from-integer spill stores garbage and the #103- + // FACE-Z field read (MOVSD-from-slot) reads it (#105). Spill + // each word from its real class. Multi-float tuples collide on + // X0 at the RETURN (#107), out of scope. Mirror of cstage + // cgen.c. Without this branch a 16B tuple receive (any element + // mix) fell to the generic single-word store below and dropped + // word1 — silent loss of t.1 (#102). + let rt16: *node = rettupleof(c, rhs); + if (rt16 != nil && sz == 16) { + let q0: *node = rt16.list; + let q1: *node = nil; + if (q0 != nil) { q1 = q0.next; }; + let q0t: *node = nil; + let q1t: *node = nil; + if (q0 != nil) { q0t = q0.lhs; }; + if (q1 != nil) { q1t = q1.lhs; }; + let e0f: bool = isfloattype(c, q0t); + let e1f: bool = isfloattype(c, q1t); + cgexpr(c, rhs); + if (e0f) { + let mov: str = "MOVSD"; + if (isf32type(c, q0t)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\tX0, "); + emitoff(off: i64); + emitline("(BP)\n"); + } else { + emitline("\tMOVQ\tAX, "); + emitoff(off: i64); + emitline("(BP)\n"); + }; + if (e1f) { + let mov: str = "MOVSD"; + if (isf32type(c, q1t)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\tX0, "); + emitoff((off + 8): i64); + emitline("(BP)\n"); + } else { + emitline("\tMOVQ\tDX, "); + emitoff((off + 8): i64); + emitline("(BP)\n"); + }; + c.lastwasreturn = 0; + return; + }; // Array literal init: `let xs: [N]T = [a, b, c];` (or [_]T). // Walk elements in declaration order, store each at off + i*esz // using the right width for the element type. Trailing `...` @@ -21504,7 +21632,7 @@ fn cgmassign(c: *cgen, n: *node) void = { let off: i32 = 0; if (l.kind == nkind.N_IDENT) { off = localfind(c, l.str); }; if (off != 0) { - tupstore(cur, off, wide); + tupstore(c, cur, off, wide, tn); }; cur = cur + tupebytes(wide); l = l.next; @@ -21576,7 +21704,7 @@ fn cgmlet(c: *cgen, n: *node) void = { let sz: i32 = 8; if (wide) { sz = tyslicesize(): i32; }; let off: i32 = localadd(c, l.str, sz, tn); - tupstore(cur, off, wide); + tupstore(c, cur, off, wide, tn); cur = cur + tupebytes(wide); l = l.next; if (pt != nil) { pt = pt.next; };