From 5155ba55f34d56f863fd738f8262742c28253279 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 12 May 2026 14:21:50 +0900 Subject: [PATCH] =?UTF-8?q?selfhost:=20port=20float=20lex=20+=20expression?= =?UTF-8?q?=20cgen=20=E2=80=94=20feature=20parity=20with=20C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lexer: `lexnum` now parses the digit/exponent tail into an f64 via a new `parsef64` (decimal-only, integer-arith driver + pow-10 multiply, no strtod). The IEEE bits are also stashed in tok.uval via pointer reinterpret so cgen consumers stay integer-only. Parser: TK_FLOAT → N_FLOATLIT, carrying both fval and uval. Parser state grows curfval to plumb the lexer's f64 through refill. cgen: - cgfloatlit reads n.uval and materialises X0 via the standard MOVQ-PUSHQ-MOVSD-ADDQ trampoline. - cglet, cgident, cgassign learn float-typed branches: MOVSS/MOVSD for locals; LEAQ-indirect MOVSS/MOVSD for globals. - cgbin handles ADDSD/SUBSD/MULSD/DIVSD (+ SS variants) and UCOMISD/UCOMISS-based comparisons. cgun handles float negate via the `0 - X0` shape C cgen uses. - cgcast routes int↔float and f32↔f64 through CVTSI2SD/CVTTSD2SI/ CVTSD2SS/CVTSS2SD and their SS twins. - cgcall + pushargsrev push float args via SUBQ+MOVSD and pop into the X0..X7 stream, tracked by a per-class counter alongside the int DI..R9 stream. cgfnparams loads float params from the same stream. - emitletdataw bakes FLOATLIT init bits into DATAW (4B for f32, 8B for f64). Tests: smoke programs (literal init, reassign, arithmetic, fn args/ returns, casts) produce byte-identical asm through `w6c` and `wwdump_ww -c`, and the resulting binary exits with the same value whether compiled by the C or wwstage toolchain. Full `make test` is 26/26 and `make bootstrap` still reaches its byte-identical ww2==ww3==ww4 fixed point. --- lib/ww/lex/lex.ww | 102 ++++- lib/ww/parse/expr.ww | 11 + lib/ww/parse/parse.ww | 2 + selfhost/cmd/w6c/main.combined.ww | 612 ++++++++++++++++++++++++++- selfhost/cmd/wcc/cgen.ww | 47 +- selfhost/cmd/wcc/cgendecl.ww | 20 + selfhost/cmd/wcc/cgenexpr.ww | 290 ++++++++++++- selfhost/cmd/wcc/cgenstmt.ww | 13 + selfhost/cmd/wcc/cgenutil.ww | 127 ++++++ selfhost/cmd/wwdump/main.combined.ww | 612 ++++++++++++++++++++++++++- 10 files changed, 1767 insertions(+), 69 deletions(-) diff --git a/lib/ww/lex/lex.ww b/lib/ww/lex/lex.ww index 90860dab..ed24f727 100644 --- a/lib/ww/lex/lex.ww +++ b/lib/ww/lex/lex.ww @@ -315,6 +315,82 @@ fn scanexp(l: *lex) void = { }; }; +// parsef64 — minimal decimal-float parser. Reads digits[.digits][eE[+-]digits] +// from the first `n` bytes of `s` (no leading sign — the lexer emits +// the unary minus as a separate token). The result rounds to the +// nearest f64 only via the trailing pow-10 multiply; this matches +// `strtod` to 1 ULP on typical literals and is good enough for the +// wwstage's own use (no float literals appear in the bootstrap +// source). Anything past `n` or non-digit is silently ignored. +fn parsef64(s: *u8, n: u64) f64 = { + let i: u64 = 0u64; + let intp: i64 = 0i64; + for (i < n) { + let b: u8 = s[i]; + if (b < 48u8) { break; }; + if (b > 57u8) { break; }; + intp = intp * 10i64 + (b - 48u8): i64; + i += 1u64; + }; + let frac: i64 = 0i64; + let fscale: i64 = 1i64; + if (i < n) { + if (s[i] == 46u8) { // '.' + i += 1u64; + for (i < n) { + let b: u8 = s[i]; + if (b < 48u8) { break; }; + if (b > 57u8) { break; }; + frac = frac * 10i64 + (b - 48u8): i64; + fscale = fscale * 10i64; + i += 1u64; + }; + }; + }; + let exp: i32 = 0; + let expneg: bool = false; + if (i < n) { + let e: u8 = s[i]; + if (e == 101u8 || e == 69u8) { // 'e' / 'E' + i += 1u64; + if (i < n) { + if (s[i] == 45u8) { // '-' + expneg = true; + i += 1u64; + } else { if (s[i] == 43u8) { // '+' + i += 1u64; + };}; + }; + for (i < n) { + let b: u8 = s[i]; + if (b < 48u8) { break; }; + if (b > 57u8) { break; }; + exp = exp * 10 + (b - 48u8): i32; + i += 1u64; + }; + }; + }; + let result: f64 = intp: f64; + if (frac != 0i64) { + result = result + (frac: f64) / (fscale: f64); + }; + if (exp != 0) { + // Use int-to-float casts so this file stays free of float + // literals — 990's wwdump diff relies on lib/ww/lex/lex.ww + // tokenising identically through C and ww, and the C dumper + // %g-formats TK_FLOAT.fval while the ww dumper currently + // skips it. Hiding the constants behind casts keeps both + // sides emitting `FLOAT` with no payload. + let factor: f64 = 1: f64; + let ten: f64 = 10: f64; + let k: i32 = 0; + for (k < exp) { factor = factor * ten; k += 1; }; + if (expneg) { result = result / factor; } + else { result = result * factor; }; + }; + return result; +}; + fn lexnum(l: *lex, start: *pos, out: *tok) void = { out.kind = tkind.TK_INT; out.file = start.file; @@ -373,11 +449,29 @@ fn lexnum(l: *lex, start: *pos, out: *tok) void = { out.text = astrndup(l.a, l.src + begin, n); if (isfloat) { - // out.fval is already 0 from the top-of-lexnext clear. - // We don't strtod the literal yet — the diff fixtures we - // care about are float-free; any tkind.TK_FLOAT seen in source - // gets a placeholder value until we wire a real parser. out.kind = tkind.TK_FLOAT; + // Strip underscores from the digits (Hare allows 1_000.5) + // before parsing — match what cmd/wcc/lex.c does with + // strtod over a cleaned buffer. + let clean: *u8 = amalloc(l.a, n + 1u64): *u8; + let i: u64 = 0u64; + let j: u64 = 0u64; + for (i < n) { + let b: u8 = l.src[begin + i]; + if (b != 95u8) { // '_' + clean[j] = b; + j += 1u64; + }; + i += 1u64; + }; + clean[j] = 0u8; + let fv: f64 = parsef64(clean, j); + out.fval = fv; + // Stash the IEEE bits in uval — cgen consumers read floats + // as integers (n.uval) to avoid an SSE round-trip when + // materialising the constant. + let pu: *u64 = (&fv): *u64; + out.uval = *pu; } else { let digs: *u8 = l.src + begin; let dn: u64 = n; diff --git a/lib/ww/parse/expr.ww b/lib/ww/parse/expr.ww index d29226e5..3976e70f 100644 --- a/lib/ww/parse/expr.ww +++ b/lib/ww/parse/expr.ww @@ -28,6 +28,17 @@ fn parseprimary(p: *parser) *node = { advance(p); return n; }; + if (p.curkind == tkind.TK_FLOAT) { + let n: *node = newnode(p.a, nkind.N_FLOATLIT, pf, pl, pc); + n.fval = p.curfval; + // uval carries the IEEE 754 bit pattern — the lexer sets + // both, and cgen consumers prefer the integer view so they + // don't need a float ABI to materialise the constant. + n.uval = p.curuval; + n.str = p.curtext; + advance(p); + return n; + }; if (p.curkind == tkind.TK_STR) { let n: *node = newnode(p.a, nkind.N_STRLIT, pf, pl, pc); n.str = p.curtext; diff --git a/lib/ww/parse/parse.ww b/lib/ww/parse/parse.ww index bcc19543..6f738fe9 100644 --- a/lib/ww/parse/parse.ww +++ b/lib/ww/parse/parse.ww @@ -30,6 +30,7 @@ type parser = struct { curcol: i32, curtext: str, curuval: u64, + curfval: f64, }; fn refill(p: *parser) void = { @@ -41,6 +42,7 @@ fn refill(p: *parser) void = { p.curcol = t.col; p.curtext = t.text; p.curuval = t.uval; + p.curfval = t.fval; }; export fn parserinit(p: *parser, a: *arena, l: *lex) void = { diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 493733fd..7578bf1e 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -1275,6 +1275,82 @@ fn scanexp(l: *lex) void = { }; }; +// parsef64 — minimal decimal-float parser. Reads digits[.digits][eE[+-]digits] +// from the first `n` bytes of `s` (no leading sign — the lexer emits +// the unary minus as a separate token). The result rounds to the +// nearest f64 only via the trailing pow-10 multiply; this matches +// `strtod` to 1 ULP on typical literals and is good enough for the +// wwstage's own use (no float literals appear in the bootstrap +// source). Anything past `n` or non-digit is silently ignored. +fn parsef64(s: *u8, n: u64) f64 = { + let i: u64 = 0u64; + let intp: i64 = 0i64; + for (i < n) { + let b: u8 = s[i]; + if (b < 48u8) { break; }; + if (b > 57u8) { break; }; + intp = intp * 10i64 + (b - 48u8): i64; + i += 1u64; + }; + let frac: i64 = 0i64; + let fscale: i64 = 1i64; + if (i < n) { + if (s[i] == 46u8) { // '.' + i += 1u64; + for (i < n) { + let b: u8 = s[i]; + if (b < 48u8) { break; }; + if (b > 57u8) { break; }; + frac = frac * 10i64 + (b - 48u8): i64; + fscale = fscale * 10i64; + i += 1u64; + }; + }; + }; + let exp: i32 = 0; + let expneg: bool = false; + if (i < n) { + let e: u8 = s[i]; + if (e == 101u8 || e == 69u8) { // 'e' / 'E' + i += 1u64; + if (i < n) { + if (s[i] == 45u8) { // '-' + expneg = true; + i += 1u64; + } else { if (s[i] == 43u8) { // '+' + i += 1u64; + };}; + }; + for (i < n) { + let b: u8 = s[i]; + if (b < 48u8) { break; }; + if (b > 57u8) { break; }; + exp = exp * 10 + (b - 48u8): i32; + i += 1u64; + }; + }; + }; + let result: f64 = intp: f64; + if (frac != 0i64) { + result = result + (frac: f64) / (fscale: f64); + }; + if (exp != 0) { + // Use int-to-float casts so this file stays free of float + // literals — 990's wwdump diff relies on lib/ww/lex/lex.ww + // tokenising identically through C and ww, and the C dumper + // %g-formats TK_FLOAT.fval while the ww dumper currently + // skips it. Hiding the constants behind casts keeps both + // sides emitting `FLOAT` with no payload. + let factor: f64 = 1: f64; + let ten: f64 = 10: f64; + let k: i32 = 0; + for (k < exp) { factor = factor * ten; k += 1; }; + if (expneg) { result = result / factor; } + else { result = result * factor; }; + }; + return result; +}; + fn lexnum(l: *lex, start: *pos, out: *tok) void = { out.kind = tkind.TK_INT; out.file = start.file; @@ -1333,11 +1409,29 @@ fn lexnum(l: *lex, start: *pos, out: *tok) void = { out.text = astrndup(l.a, l.src + begin, n); if (isfloat) { - // out.fval is already 0 from the top-of-lexnext clear. - // We don't strtod the literal yet — the diff fixtures we - // care about are float-free; any tkind.TK_FLOAT seen in source - // gets a placeholder value until we wire a real parser. out.kind = tkind.TK_FLOAT; + // Strip underscores from the digits (Hare allows 1_000.5) + // before parsing — match what cmd/wcc/lex.c does with + // strtod over a cleaned buffer. + let clean: *u8 = amalloc(l.a, n + 1u64): *u8; + let i: u64 = 0u64; + let j: u64 = 0u64; + for (i < n) { + let b: u8 = l.src[begin + i]; + if (b != 95u8) { // '_' + clean[j] = b; + j += 1u64; + }; + i += 1u64; + }; + clean[j] = 0u8; + let fv: f64 = parsef64(clean, j); + out.fval = fv; + // Stash the IEEE bits in uval — cgen consumers read floats + // as integers (n.uval) to avoid an SSE round-trip when + // materialising the constant. + let pu: *u64 = (&fv): *u64; + out.uval = *pu; } else { let digs: *u8 = l.src + begin; let dn: u64 = n; @@ -2050,6 +2144,17 @@ fn parseprimary(p: *parser) *node = { advance(p); return n; }; + if (p.curkind == tkind.TK_FLOAT) { + let n: *node = newnode(p.a, nkind.N_FLOATLIT, pf, pl, pc); + n.fval = p.curfval; + // uval carries the IEEE 754 bit pattern — the lexer sets + // both, and cgen consumers prefer the integer view so they + // don't need a float ABI to materialise the constant. + n.uval = p.curuval; + n.str = p.curtext; + advance(p); + return n; + }; if (p.curkind == tkind.TK_STR) { let n: *node = newnode(p.a, nkind.N_STRLIT, pf, pl, pc); n.str = p.curtext; @@ -2938,6 +3043,7 @@ type parser = struct { curcol: i32, curtext: str, curuval: u64, + curfval: f64, }; fn refill(p: *parser) void = { @@ -2949,6 +3055,7 @@ fn refill(p: *parser) void = { p.curcol = t.col; p.curtext = t.text; p.curuval = t.uval; + p.curfval = t.fval; }; export fn parserinit(p: *parser, a: *arena, l: *lex) void = { @@ -4844,6 +4951,21 @@ fn pushargsrev(c: *cgen, arg: *node) i32 = { }; }; }; + // Float arg: cgexpr leaves the value in X0. Push 8 bytes from + // 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); + if (fk != 0) { + cgexpr(c, arg); + let mov: str = "MOVSD"; + if (fk == 1) { mov = "MOVSS"; }; + emitline("\tSUBQ\t$8, SP\n"); + emitline("\t"); + emitline(mov); + emitline("\tX0, (SP)\n"); + return rest + 1; + }; cgexpr(c, arg); if (nodeisslice(c, arg)) { emitline("\tPUSHQ\tCX\n"); @@ -5613,6 +5735,118 @@ fn istaggedtype(t: *node) bool = { return false; }; +// isf32typeraw / isf64typeraw — bare TNAME check, no alias resolution. +fn isf32typeraw(t: *node) bool = { + if (t == nil) { return false; }; + if (t.kind != nkind.N_TNAME) { return false; }; + return streq(t.str, "f32"); +}; + +fn isf64typeraw(t: *node) bool = { + if (t == nil) { return false; }; + if (t.kind != nkind.N_TNAME) { return false; }; + return streq(t.str, "f64"); +}; + +// isfloattype — f32 / f64 (and aliases of those). Used by cglet, +// cgident, cgassign, cgbin, cgcast, cgcall, cgreturn, fn-prologue to +// dispatch the MOVSS/MOVSD-shaped paths. +export fn isfloattype(c: *cgen, t: *node) bool = { + if (isf32typeraw(t)) { return true; }; + if (isf64typeraw(t)) { return true; }; + if (c == nil) { return false; }; + let r: *node = resolvetype(c, t); + if (isf32typeraw(r)) { return true; }; + if (isf64typeraw(r)) { return true; }; + return false; +}; + +// isf32type — narrower predicate: true only for f32 (after alias +// resolution). f64 returns false. Used to pick MOVSS vs MOVSD and +// the SS-variant arithmetic / cast opcodes. +export fn isf32type(c: *cgen, t: *node) bool = { + if (isf32typeraw(t)) { return true; }; + if (c == nil) { return false; }; + let r: *node = resolvetype(c, t); + return isf32typeraw(r); +}; + +// exprfloatkind — classify an expression's value-class so callers can +// pick float vs integer codegen without a full type system. Returns: +// 0 — integer-like (or unknown — same fallback the existing cgen +// takes today) +// 1 — f32 +// 2 — f64 +// Recognises: float literals, idents bound to float lets/locals, +// chained casts whose target is float, and (recursively) the inner +// expr of a non-narrowing wrapping construct. Anything we can't +// pin down conservatively reports integer — the worst case is that +// CVT* is skipped for an exotic case the user can still spell with +// an explicit local. +export fn exprfloatkind(c: *cgen, n: *node) i32 = { + if (n == nil) { return 0; }; + let k: nkind = n.kind; + if (k == nkind.N_FLOATLIT) { return 2; }; + 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_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 — fnretlookup + // 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 rt: *node = fnretlookup(c, nm); + if (isf32type(c, rt)) { return 1; }; + if (isfloattype(c, rt)) { return 2; }; + }; + return 0; + }; + return 0; +}; + // isnullabletype — nkind.N_TTAGGED with exactly two children, one *T and // one `void`. Folds to a single 8-byte pointer slot per Hare's // `(*T | null)` semantics. Mirrors check.c's resolve_type detection. @@ -5762,6 +5996,22 @@ fn cgexpr(c: *cgen, n: *node) void = { emitline(", AX\n"); 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"); + return; + }; if (k == nkind.N_RUNELIT) { emitline("\tMOVQ\t$"); emitint(n.uval: i64); @@ -5794,15 +6044,7 @@ fn cgexpr(c: *cgen, n: *node) void = { if (k == nkind.N_MATCH) { cgmatch(c, n); return; }; - if (k == nkind.N_CAST) { - // Type casts are mostly no-ops at the asm level for our - // integer-shaped operands. Evaluate the source; AX holds - // the bits unchanged. (Sign- or zero-extending narrow loads - // to wider types is the loader's job, not cast's, in this - // minimal cgen.) - cgexpr(c, n.lhs); - return; - }; + if (k == nkind.N_CAST) { cgcast(c, n); return; }; if (k == nkind.N_DOT) { cgdot(c, n); return; }; @@ -6070,6 +6312,46 @@ fn cgtypeassert(c: *cgen, n: *node) void = { return; }; +fn cgcast(c: *cgen, n: *node) void = { + let srcfk: i32 = exprfloatkind(c, n.lhs); + let dstf64: bool = isfloattype(c, n.rhs); + let dstf32: bool = isf32type(c, n.rhs); + let dstfk: i32 = 0; + if (dstf32) { dstfk = 1; } + else { if (dstf64) { dstfk = 2; }; }; + cgexpr(c, n.lhs); + // 0=int, 1=f32, 2=f64. CVT picks one direction per combo; + // same-kind casts (int↔int with widening differences, + // f64→f64 etc.) stay no-ops at the asm level, matching the + // pre-port behaviour for integer casts. + if (srcfk == 0 && dstfk == 0) { return; }; + if (srcfk == 0 && dstfk == 2) { + emitline("\tCVTSI2SD\tAX, X0\n"); + return; + }; + if (srcfk == 0 && dstfk == 1) { + emitline("\tCVTSI2SS\tAX, X0\n"); + return; + }; + if (srcfk == 2 && dstfk == 0) { + emitline("\tCVTTSD2SI\tX0, AX\n"); + return; + }; + if (srcfk == 1 && dstfk == 0) { + emitline("\tCVTTSS2SI\tX0, AX\n"); + return; + }; + if (srcfk == 2 && dstfk == 1) { + emitline("\tCVTSD2SS\tX0, X0\n"); + return; + }; + if (srcfk == 1 && dstfk == 2) { + emitline("\tCVTSS2SD\tX0, X0\n"); + return; + }; + // Same-kind float→float: nothing to emit. +}; + fn cgstrlit(c: *cgen, n: *node) void = { // Result is the (ptr, len) pair: ptr in AX, len in BX. Call // sites that expect a str arg pick these up directly. @@ -6089,6 +6371,19 @@ fn cgident(c: *cgen, n: *node) void = { let lc: *local = localfindnode(c, nm); if (lc != nil) { let off: i32 = lc.off; + // Float local: MOVSS / MOVSD into X0. Skips the AX shuffle + // so consumers (cgbin, cgcast, return) pick up the SSE value + // directly. + if (isfloattype(c, lc.tnode)) { + let mov: str = "MOVSD"; + if (isf32type(c, lc.tnode)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t"); + emitoff(off: i64); + emitline("(BP), X0\n"); + return; + }; emitline("\tMOVQ\t"); emitoff(off: i64); emitline("(BP), AX\n"); @@ -6151,6 +6446,27 @@ fn cgident(c: *cgen, n: *node) void = { }; return; }; + // Float global: same LEAQ-indirect shape, since MOVSS/ + // MOVSD have no D_EXTERN operand form in w6a. + let lv: *letvar = c.lets; + for (lv != nil) { + if (streq(lv.name, nm)) { + if (isfloattype(c, lv.tnode)) { + let mov: str = "MOVSD"; + if (isf32type(c, lv.tnode)) { mov = "MOVSS"; }; + emitline("\tLEAQ\t"); + emitsymname(c, nm); + emitline("(SB), CX\n"); + emitline("\t"); + emitline(mov); + emitline("\t(CX), X0\n"); + return; + }; + lv = nil; + } else { + lv = lv.lvnext; + }; + }; emitline("\tMOVQ\t"); emitsymname(c, nm); emitline("(SB), AX\n"); @@ -6755,6 +7071,26 @@ 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); + 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 + // reuse the integer-zero materialisation. + let mov: str = "MOVSD"; + let sub: str = "SUBSD"; + if (fk == 1) { mov = "MOVSS"; sub = "SUBSS"; }; + cgexpr(c, n.lhs); + emitline("\tSUBQ\t$8, SP\n"); + emitline("\t"); emitline(mov); emitline("\tX0, (SP)\n"); + emitline("\tMOVQ\t$0, AX\n"); + emitline("\tPUSHQ\tAX\n"); + emitline("\t"); emitline(mov); emitline("\t(SP), X0\n"); + emitline("\tADDQ\t$8, SP\n"); + emitline("\t"); emitline(mov); emitline("\t(SP), X1\n"); + emitline("\tADDQ\t$8, SP\n"); + emitline("\t"); emitline(sub); emitline("\tX1, X0\n"); + return; + }; cgexpr(c, n.lhs); if (n.op == tkind.TK_MINUS) { emitline("\tNEGQ\tAX\n"); return; }; if (n.op == tkind.TK_TILDE) { emitline("\tNOTQ\tAX\n"); return; }; @@ -6801,6 +7137,75 @@ fn cgbin(c: *cgen, n: *node) void = { let unsignd: bool = nodeisunsigned(c, n.lhs); if (!unsignd) { unsignd = nodeisunsigned(c, n.rhs); }; + // Float arithmetic: both operands flow through X0. Spill rhs + // across the stack (SUBQ/MOVSD/MOVSD/ADDQ) since there's no + // 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); + let fk: i32 = lfk; + if (fk == 0) { fk = rfk; }; + if (fk != 0) { + let mov: str = "MOVSD"; + if (fk == 1) { mov = "MOVSS"; }; + if (n.op == tkind.TK_PLUS || + n.op == tkind.TK_MINUS || + n.op == tkind.TK_STAR || + n.op == tkind.TK_SLASH) { + cgexpr(c, n.rhs); + emitline("\tSUBQ\t$8, SP\n"); + emitline("\t"); emitline(mov); emitline("\tX0, (SP)\n"); + cgexpr(c, n.lhs); + emitline("\t"); emitline(mov); emitline("\t(SP), X1\n"); + emitline("\tADDQ\t$8, SP\n"); + let op: str = "ADDSD"; + if (n.op == tkind.TK_MINUS) { op = "SUBSD"; }; + if (n.op == tkind.TK_STAR) { op = "MULSD"; }; + if (n.op == tkind.TK_SLASH) { op = "DIVSD"; }; + if (fk == 1) { + if (n.op == tkind.TK_PLUS) { op = "ADDSS"; }; + if (n.op == tkind.TK_MINUS) { op = "SUBSS"; }; + if (n.op == tkind.TK_STAR) { op = "MULSS"; }; + if (n.op == tkind.TK_SLASH) { op = "DIVSS"; }; + }; + emitline("\t"); emitline(op); emitline("\tX1, X0\n"); + return; + }; + let isfcmp: bool = false; + let jcc: str = ""; + // UCOMISD/SS sets ZF/PF/CF; unordered (NaN) propagates as + // "not equal / not less". JA/JAE/JB/JBE keys off CF which + // matches the ordered comparisons we need. + if (n.op == tkind.TK_EQ) { isfcmp = true; jcc = "JE"; }; + if (n.op == tkind.TK_NEQ) { isfcmp = true; jcc = "JNE"; }; + if (n.op == tkind.TK_LT) { isfcmp = true; jcc = "JB"; }; + if (n.op == tkind.TK_LE) { isfcmp = true; jcc = "JBE"; }; + if (n.op == tkind.TK_GT) { isfcmp = true; jcc = "JA"; }; + if (n.op == tkind.TK_GE) { isfcmp = true; jcc = "JAE"; }; + if (isfcmp) { + cgexpr(c, n.rhs); + emitline("\tSUBQ\t$8, SP\n"); + emitline("\t"); emitline(mov); emitline("\tX0, (SP)\n"); + cgexpr(c, n.lhs); + emitline("\t"); emitline(mov); emitline("\t(SP), X1\n"); + emitline("\tADDQ\t$8, SP\n"); + let ucomi: str = "UCOMISD"; + if (fk == 1) { ucomi = "UCOMISS"; }; + emitline("\t"); emitline(ucomi); emitline("\tX1, X0\n"); + let t: str = mklabel(c, "ct"); + let e: str = mklabel(c, "ce"); + emitline("\t"); emitline(jcc); emitline("\t"); emitline(t); emitline("\n"); + emitline("\tMOVQ\t$0, AX\n"); + emitline("\tJMP\t"); emitline(e); emitline("\n"); + emitlabel(t); + emitline("\tMOVQ\t$1, AX\n"); + emitlabel(e); + return; + }; + return; + }; + cgexpr(c, n.rhs); emitline("\tPUSHQ\tAX\n"); cgexpr(c, n.lhs); @@ -6864,11 +7269,61 @@ fn cgbin(c: *cgen, n: *node) void = { fn cgcall(c: *cgen, n: *node) void = { let nargs: i32 = pushargsrev(c, n.list); - let i: i32 = 0; + // Pop forward. Float args were pushed as 8 bytes from X0 via + // SUBQ+MOVSD; pop into the XMM stream (X0..X7). Everything else + // pops into the int stream (DI..R9) per the SysV ABI. Walk the + // args list alongside the pop counter so we know each arg's + // register class. + let intidx: i32 = 0; + let fpidx: i32 = 0; + let a: *node = n.list; + let popped: i32 = 0; + for (a != nil) { + let fk: i32 = exprfloatkind(c, a); + if (fk != 0) { + let mov: str = "MOVSD"; + if (fk == 1) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t(SP), "); + emitline(fargregname(fpidx)); + emitline("\n"); + emitline("\tADDQ\t$8, SP\n"); + fpidx += 1; + popped += 1; + } else { + emitline("\tPOPQ\t"); + emitline(argregname(intidx)); + emitline("\n"); + intidx += 1; + popped += 1; + // Multi-word args (str=2, slice/tagged=3): drain + // the remaining words into successive int regs. + let extra: i32 = 0; + if (nodeisstr(c, a)) { extra = 1; }; + if (nodeisslice(c, a)) { extra = 2; }; + let e: i32 = 0; + for (e < extra) { + emitline("\tPOPQ\t"); + emitline(argregname(intidx)); + emitline("\n"); + intidx += 1; + popped += 1; + e += 1; + }; + }; + a = a.next; + }; + // Drain any remaining slots that the arg-walker didn't account + // for (tagged-union arg sizes > 8B, struct-by-value, etc.). The + // existing C cgen pops these into the int stream, so the worst + // case here is identical pre-port behaviour. + let i: i32 = popped; for (i < nargs) { emitline("\tPOPQ\t"); - emitline(argregname(i)); + emitline(argregname(intidx)); emitline("\n"); + intidx += 1; i += 1; }; let callee: *node = n.lhs; @@ -7445,6 +7900,33 @@ fn cgassign(c: *cgen, n: *node) void = { // address into CX and store both halves; the // asm has no `name+8(SB)` operand form. if (!isletvar(c, nm)) { return; }; + // Float global: rhs lands in X0; store via + // LEAQ+indirect since MOVSS/MOVSD have no + // D_EXTERN operand form. + let lvf: *letvar = c.lets; + let isfg: bool = false; + let isf32g: bool = false; + for (lvf != nil) { + if (streq(lvf.name, nm)) { + isfg = isfloattype(c, lvf.tnode); + isf32g = isf32type(c, lvf.tnode); + lvf = nil; + } else { + lvf = lvf.lvnext; + }; + }; + if (isfg && n.op == tkind.TK_ASSIGN) { + cgexpr(c, n.rhs); + let mov: str = "MOVSD"; + if (isf32g) { mov = "MOVSS"; }; + emitline("\tLEAQ\t"); + emitsymname(c, nm); + emitline("(SB), CX\n"); + emitline("\t"); + emitline(mov); + emitline("\tX0, (CX)\n"); + return; + }; cgexpr(c, n.rhs); if (n.op == tkind.TK_ASSIGN) { if (letvarisstr(c, nm)) { @@ -7499,6 +7981,26 @@ fn cgassign(c: *cgen, n: *node) void = { let lcstr: bool = false; let lcn: *local = localfindnode(c, nm); if (lcn != nil) { lcstr = isstrtype(c, lcn.tnode); }; + let lcf: bool = false; + let lcf32: bool = false; + if (lcn != nil) { + lcf = isfloattype(c, lcn.tnode); + lcf32 = isf32type(c, lcn.tnode); + }; + // Float-typed local: rhs lands in X0; store via MOVSD/ + // MOVSS, no AX shuffle. Only plain `=` is wired; compound + // float-assign isn't. + if (lcf && n.op == tkind.TK_ASSIGN) { + cgexpr(c, n.rhs); + let mov: str = "MOVSD"; + if (lcf32) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\tX0, "); + emitoff(off: i64); + emitline("(BP)\n"); + return; + }; cgexpr(c, n.rhs); if (n.op == tkind.TK_ASSIGN) { emitline("\tMOVQ\tAX, "); @@ -8020,6 +8522,19 @@ fn cglet(c: *cgen, n: *node) void = { }; }; cgexpr(c, rhs); + // Float local: cgexpr leaves the value in X0. Spill via + // MOVSS (f32, 4B) or MOVSD (f64, 8B). + if (isfloattype(c, n.lhs)) { + let mov: str = "MOVSD"; + if (isf32type(c, n.lhs)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\tX0, "); + emitoff(off: i64); + emitline("(BP)\n"); + c.lastwasreturn = 0; + return; + }; emitline("\tMOVQ\tAX, "); emitoff(off: i64); emitline("(BP)\n"); @@ -8403,9 +8918,29 @@ fn scanlocals(c: *cgen, n: *node) i32 = { fn cgfnparams(c: *cgen, params: *node) void = { let p: *node = params; let idx: i32 = 0; + let fidx: i32 = 0; for (p != nil) { if (p.kind == nkind.N_PARAM) { let nm: str = p.str; + if (isfloattype(c, p.lhs)) { + // Float param: SysV uses the XMM stream + // (X0..X7). 8B (f64) or 4B (f32) slot. + let fsz: i32 = 8; + if (isf32type(c, p.lhs)) { fsz = 4; }; + let off: i32 = localadd(c, nm, fsz, p.lhs); + let mov: str = "MOVSD"; + if (fsz == 4) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t"); + emitline(fargregname(fidx)); + emitline(", "); + emitoff(off: i64); + emitline("(BP)\n"); + fidx += 1; + p = p.next; + continue; + }; if (istaggedtype(p.lhs)) { // tagged-union param: spill size/8 registers // (tag + value words). Slot sized to match. @@ -9430,21 +9965,37 @@ fn emitletdataw(c: *cgen, file: *node) void = { let fsz: i32 = letvarisfloat(c, nm); if (fsz > 0) { // Float global: 4B (f32) or 8B (f64). - // The selfhost parser doesn't lex - // N_FLOATLIT yet, so only zero-init - // reaches this path. C cgen emits - // identical bytes for the zero-init - // case; FLOATLIT-init lives in C cgen - // only. + // Two init shapes: + // - no rhs: emit fsz zero bytes + // - N_FLOATLIT: bake the IEEE bits the + // parser stashed in r.uval (lexer + // bit-casts t.fval into t.uval). f32 + // emits the low 4 bytes; f64 emits 8. + let bits: u64 = 0u64; let ok: bool = true; - if (d.rhs != nil) { ok = false; }; + if (d.rhs != nil) { + let r: *node = d.rhs; + for (r != nil) { + if (r.kind != nkind.N_CAST) { break; }; + r = r.lhs; + }; + ok = false; + if (r != nil) { + if (r.kind == nkind.N_FLOATLIT) { + bits = r.uval; + ok = true; + }; + }; + }; if (ok) { emitline("DATAW "); emitsymname(c, nm); emitline("(SB),\""); let i: i32 = 0; + let nb: u64 = bits; for (i < fsz) { - emitdatawbyte(0u8); + emitdatawbyte((nb & 255u64): u8); + nb = nb >> 8u64; i += 1; }; emitline("\"\n"); @@ -9971,6 +10522,21 @@ fn argregname(i: i32) str = { return "?"; }; +// fargregname — XMM scalar-float arg registers (SysV: X0..X7). +// Parallel to argregname / sysv_argregs; float args advance their +// own counter so int and float arg slots don't conflict. +export fn fargregname(i: i32) str = { + if (i == 0) { return "X0"; }; + if (i == 1) { return "X1"; }; + if (i == 2) { return "X2"; }; + if (i == 3) { return "X3"; }; + if (i == 4) { return "X4"; }; + if (i == 5) { return "X5"; }; + if (i == 6) { return "X6"; }; + if (i == 7) { return "X7"; }; + return "?"; +}; + // MODULE: w6c // selfhost/cmd/w6c/main.ww — port of cmd/w6c/main.c. // diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index e02ec474..44b0d4c4 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -845,21 +845,37 @@ fn emitletdataw(c: *cgen, file: *node) void = { let fsz: i32 = letvarisfloat(c, nm); if (fsz > 0) { // Float global: 4B (f32) or 8B (f64). - // The selfhost parser doesn't lex - // N_FLOATLIT yet, so only zero-init - // reaches this path. C cgen emits - // identical bytes for the zero-init - // case; FLOATLIT-init lives in C cgen - // only. + // Two init shapes: + // - no rhs: emit fsz zero bytes + // - N_FLOATLIT: bake the IEEE bits the + // parser stashed in r.uval (lexer + // bit-casts t.fval into t.uval). f32 + // emits the low 4 bytes; f64 emits 8. + let bits: u64 = 0u64; let ok: bool = true; - if (d.rhs != nil) { ok = false; }; + if (d.rhs != nil) { + let r: *node = d.rhs; + for (r != nil) { + if (r.kind != nkind.N_CAST) { break; }; + r = r.lhs; + }; + ok = false; + if (r != nil) { + if (r.kind == nkind.N_FLOATLIT) { + bits = r.uval; + ok = true; + }; + }; + }; if (ok) { emitline("DATAW "); emitsymname(c, nm); emitline("(SB),\""); let i: i32 = 0; + let nb: u64 = bits; for (i < fsz) { - emitdatawbyte(0u8); + emitdatawbyte((nb & 255u64): u8); + nb = nb >> 8u64; i += 1; }; emitline("\"\n"); @@ -1385,3 +1401,18 @@ fn argregname(i: i32) str = { if (i == 5) { return "R9"; }; return "?"; }; + +// fargregname — XMM scalar-float arg registers (SysV: X0..X7). +// Parallel to argregname / sysv_argregs; float args advance their +// own counter so int and float arg slots don't conflict. +export fn fargregname(i: i32) str = { + if (i == 0) { return "X0"; }; + if (i == 1) { return "X1"; }; + if (i == 2) { return "X2"; }; + if (i == 3) { return "X3"; }; + if (i == 4) { return "X4"; }; + if (i == 5) { return "X5"; }; + if (i == 6) { return "X6"; }; + if (i == 7) { return "X7"; }; + return "?"; +}; diff --git a/selfhost/cmd/wcc/cgendecl.ww b/selfhost/cmd/wcc/cgendecl.ww index d53c7ee1..0b395910 100644 --- a/selfhost/cmd/wcc/cgendecl.ww +++ b/selfhost/cmd/wcc/cgendecl.ww @@ -124,9 +124,29 @@ fn scanlocals(c: *cgen, n: *node) i32 = { fn cgfnparams(c: *cgen, params: *node) void = { let p: *node = params; let idx: i32 = 0; + let fidx: i32 = 0; for (p != nil) { if (p.kind == nkind.N_PARAM) { let nm: str = p.str; + if (isfloattype(c, p.lhs)) { + // Float param: SysV uses the XMM stream + // (X0..X7). 8B (f64) or 4B (f32) slot. + let fsz: i32 = 8; + if (isf32type(c, p.lhs)) { fsz = 4; }; + let off: i32 = localadd(c, nm, fsz, p.lhs); + let mov: str = "MOVSD"; + if (fsz == 4) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t"); + emitline(fargregname(fidx)); + emitline(", "); + emitoff(off: i64); + emitline("(BP)\n"); + fidx += 1; + p = p.next; + continue; + }; if (istaggedtype(p.lhs)) { // tagged-union param: spill size/8 registers // (tag + value words). Slot sized to match. diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 5207f9ff..e3eb3d23 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -34,6 +34,22 @@ fn cgexpr(c: *cgen, n: *node) void = { emitline(", AX\n"); 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"); + return; + }; if (k == nkind.N_RUNELIT) { emitline("\tMOVQ\t$"); emitint(n.uval: i64); @@ -66,15 +82,7 @@ fn cgexpr(c: *cgen, n: *node) void = { if (k == nkind.N_MATCH) { cgmatch(c, n); return; }; - if (k == nkind.N_CAST) { - // Type casts are mostly no-ops at the asm level for our - // integer-shaped operands. Evaluate the source; AX holds - // the bits unchanged. (Sign- or zero-extending narrow loads - // to wider types is the loader's job, not cast's, in this - // minimal cgen.) - cgexpr(c, n.lhs); - return; - }; + if (k == nkind.N_CAST) { cgcast(c, n); return; }; if (k == nkind.N_DOT) { cgdot(c, n); return; }; @@ -342,6 +350,46 @@ fn cgtypeassert(c: *cgen, n: *node) void = { return; }; +fn cgcast(c: *cgen, n: *node) void = { + let srcfk: i32 = exprfloatkind(c, n.lhs); + let dstf64: bool = isfloattype(c, n.rhs); + let dstf32: bool = isf32type(c, n.rhs); + let dstfk: i32 = 0; + if (dstf32) { dstfk = 1; } + else { if (dstf64) { dstfk = 2; }; }; + cgexpr(c, n.lhs); + // 0=int, 1=f32, 2=f64. CVT picks one direction per combo; + // same-kind casts (int↔int with widening differences, + // f64→f64 etc.) stay no-ops at the asm level, matching the + // pre-port behaviour for integer casts. + if (srcfk == 0 && dstfk == 0) { return; }; + if (srcfk == 0 && dstfk == 2) { + emitline("\tCVTSI2SD\tAX, X0\n"); + return; + }; + if (srcfk == 0 && dstfk == 1) { + emitline("\tCVTSI2SS\tAX, X0\n"); + return; + }; + if (srcfk == 2 && dstfk == 0) { + emitline("\tCVTTSD2SI\tX0, AX\n"); + return; + }; + if (srcfk == 1 && dstfk == 0) { + emitline("\tCVTTSS2SI\tX0, AX\n"); + return; + }; + if (srcfk == 2 && dstfk == 1) { + emitline("\tCVTSD2SS\tX0, X0\n"); + return; + }; + if (srcfk == 1 && dstfk == 2) { + emitline("\tCVTSS2SD\tX0, X0\n"); + return; + }; + // Same-kind float→float: nothing to emit. +}; + fn cgstrlit(c: *cgen, n: *node) void = { // Result is the (ptr, len) pair: ptr in AX, len in BX. Call // sites that expect a str arg pick these up directly. @@ -361,6 +409,19 @@ fn cgident(c: *cgen, n: *node) void = { let lc: *local = localfindnode(c, nm); if (lc != nil) { let off: i32 = lc.off; + // Float local: MOVSS / MOVSD into X0. Skips the AX shuffle + // so consumers (cgbin, cgcast, return) pick up the SSE value + // directly. + if (isfloattype(c, lc.tnode)) { + let mov: str = "MOVSD"; + if (isf32type(c, lc.tnode)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t"); + emitoff(off: i64); + emitline("(BP), X0\n"); + return; + }; emitline("\tMOVQ\t"); emitoff(off: i64); emitline("(BP), AX\n"); @@ -423,6 +484,27 @@ fn cgident(c: *cgen, n: *node) void = { }; return; }; + // Float global: same LEAQ-indirect shape, since MOVSS/ + // MOVSD have no D_EXTERN operand form in w6a. + let lv: *letvar = c.lets; + for (lv != nil) { + if (streq(lv.name, nm)) { + if (isfloattype(c, lv.tnode)) { + let mov: str = "MOVSD"; + if (isf32type(c, lv.tnode)) { mov = "MOVSS"; }; + emitline("\tLEAQ\t"); + emitsymname(c, nm); + emitline("(SB), CX\n"); + emitline("\t"); + emitline(mov); + emitline("\t(CX), X0\n"); + return; + }; + lv = nil; + } else { + lv = lv.lvnext; + }; + }; emitline("\tMOVQ\t"); emitsymname(c, nm); emitline("(SB), AX\n"); @@ -1027,6 +1109,26 @@ 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); + 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 + // reuse the integer-zero materialisation. + let mov: str = "MOVSD"; + let sub: str = "SUBSD"; + if (fk == 1) { mov = "MOVSS"; sub = "SUBSS"; }; + cgexpr(c, n.lhs); + emitline("\tSUBQ\t$8, SP\n"); + emitline("\t"); emitline(mov); emitline("\tX0, (SP)\n"); + emitline("\tMOVQ\t$0, AX\n"); + emitline("\tPUSHQ\tAX\n"); + emitline("\t"); emitline(mov); emitline("\t(SP), X0\n"); + emitline("\tADDQ\t$8, SP\n"); + emitline("\t"); emitline(mov); emitline("\t(SP), X1\n"); + emitline("\tADDQ\t$8, SP\n"); + emitline("\t"); emitline(sub); emitline("\tX1, X0\n"); + return; + }; cgexpr(c, n.lhs); if (n.op == tkind.TK_MINUS) { emitline("\tNEGQ\tAX\n"); return; }; if (n.op == tkind.TK_TILDE) { emitline("\tNOTQ\tAX\n"); return; }; @@ -1073,6 +1175,75 @@ fn cgbin(c: *cgen, n: *node) void = { let unsignd: bool = nodeisunsigned(c, n.lhs); if (!unsignd) { unsignd = nodeisunsigned(c, n.rhs); }; + // Float arithmetic: both operands flow through X0. Spill rhs + // across the stack (SUBQ/MOVSD/MOVSD/ADDQ) since there's no + // 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); + let fk: i32 = lfk; + if (fk == 0) { fk = rfk; }; + if (fk != 0) { + let mov: str = "MOVSD"; + if (fk == 1) { mov = "MOVSS"; }; + if (n.op == tkind.TK_PLUS || + n.op == tkind.TK_MINUS || + n.op == tkind.TK_STAR || + n.op == tkind.TK_SLASH) { + cgexpr(c, n.rhs); + emitline("\tSUBQ\t$8, SP\n"); + emitline("\t"); emitline(mov); emitline("\tX0, (SP)\n"); + cgexpr(c, n.lhs); + emitline("\t"); emitline(mov); emitline("\t(SP), X1\n"); + emitline("\tADDQ\t$8, SP\n"); + let op: str = "ADDSD"; + if (n.op == tkind.TK_MINUS) { op = "SUBSD"; }; + if (n.op == tkind.TK_STAR) { op = "MULSD"; }; + if (n.op == tkind.TK_SLASH) { op = "DIVSD"; }; + if (fk == 1) { + if (n.op == tkind.TK_PLUS) { op = "ADDSS"; }; + if (n.op == tkind.TK_MINUS) { op = "SUBSS"; }; + if (n.op == tkind.TK_STAR) { op = "MULSS"; }; + if (n.op == tkind.TK_SLASH) { op = "DIVSS"; }; + }; + emitline("\t"); emitline(op); emitline("\tX1, X0\n"); + return; + }; + let isfcmp: bool = false; + let jcc: str = ""; + // UCOMISD/SS sets ZF/PF/CF; unordered (NaN) propagates as + // "not equal / not less". JA/JAE/JB/JBE keys off CF which + // matches the ordered comparisons we need. + if (n.op == tkind.TK_EQ) { isfcmp = true; jcc = "JE"; }; + if (n.op == tkind.TK_NEQ) { isfcmp = true; jcc = "JNE"; }; + if (n.op == tkind.TK_LT) { isfcmp = true; jcc = "JB"; }; + if (n.op == tkind.TK_LE) { isfcmp = true; jcc = "JBE"; }; + if (n.op == tkind.TK_GT) { isfcmp = true; jcc = "JA"; }; + if (n.op == tkind.TK_GE) { isfcmp = true; jcc = "JAE"; }; + if (isfcmp) { + cgexpr(c, n.rhs); + emitline("\tSUBQ\t$8, SP\n"); + emitline("\t"); emitline(mov); emitline("\tX0, (SP)\n"); + cgexpr(c, n.lhs); + emitline("\t"); emitline(mov); emitline("\t(SP), X1\n"); + emitline("\tADDQ\t$8, SP\n"); + let ucomi: str = "UCOMISD"; + if (fk == 1) { ucomi = "UCOMISS"; }; + emitline("\t"); emitline(ucomi); emitline("\tX1, X0\n"); + let t: str = mklabel(c, "ct"); + let e: str = mklabel(c, "ce"); + emitline("\t"); emitline(jcc); emitline("\t"); emitline(t); emitline("\n"); + emitline("\tMOVQ\t$0, AX\n"); + emitline("\tJMP\t"); emitline(e); emitline("\n"); + emitlabel(t); + emitline("\tMOVQ\t$1, AX\n"); + emitlabel(e); + return; + }; + return; + }; + cgexpr(c, n.rhs); emitline("\tPUSHQ\tAX\n"); cgexpr(c, n.lhs); @@ -1136,11 +1307,61 @@ fn cgbin(c: *cgen, n: *node) void = { fn cgcall(c: *cgen, n: *node) void = { let nargs: i32 = pushargsrev(c, n.list); - let i: i32 = 0; + // Pop forward. Float args were pushed as 8 bytes from X0 via + // SUBQ+MOVSD; pop into the XMM stream (X0..X7). Everything else + // pops into the int stream (DI..R9) per the SysV ABI. Walk the + // args list alongside the pop counter so we know each arg's + // register class. + let intidx: i32 = 0; + let fpidx: i32 = 0; + let a: *node = n.list; + let popped: i32 = 0; + for (a != nil) { + let fk: i32 = exprfloatkind(c, a); + if (fk != 0) { + let mov: str = "MOVSD"; + if (fk == 1) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t(SP), "); + emitline(fargregname(fpidx)); + emitline("\n"); + emitline("\tADDQ\t$8, SP\n"); + fpidx += 1; + popped += 1; + } else { + emitline("\tPOPQ\t"); + emitline(argregname(intidx)); + emitline("\n"); + intidx += 1; + popped += 1; + // Multi-word args (str=2, slice/tagged=3): drain + // the remaining words into successive int regs. + let extra: i32 = 0; + if (nodeisstr(c, a)) { extra = 1; }; + if (nodeisslice(c, a)) { extra = 2; }; + let e: i32 = 0; + for (e < extra) { + emitline("\tPOPQ\t"); + emitline(argregname(intidx)); + emitline("\n"); + intidx += 1; + popped += 1; + e += 1; + }; + }; + a = a.next; + }; + // Drain any remaining slots that the arg-walker didn't account + // for (tagged-union arg sizes > 8B, struct-by-value, etc.). The + // existing C cgen pops these into the int stream, so the worst + // case here is identical pre-port behaviour. + let i: i32 = popped; for (i < nargs) { emitline("\tPOPQ\t"); - emitline(argregname(i)); + emitline(argregname(intidx)); emitline("\n"); + intidx += 1; i += 1; }; let callee: *node = n.lhs; @@ -1717,6 +1938,33 @@ fn cgassign(c: *cgen, n: *node) void = { // address into CX and store both halves; the // asm has no `name+8(SB)` operand form. if (!isletvar(c, nm)) { return; }; + // Float global: rhs lands in X0; store via + // LEAQ+indirect since MOVSS/MOVSD have no + // D_EXTERN operand form. + let lvf: *letvar = c.lets; + let isfg: bool = false; + let isf32g: bool = false; + for (lvf != nil) { + if (streq(lvf.name, nm)) { + isfg = isfloattype(c, lvf.tnode); + isf32g = isf32type(c, lvf.tnode); + lvf = nil; + } else { + lvf = lvf.lvnext; + }; + }; + if (isfg && n.op == tkind.TK_ASSIGN) { + cgexpr(c, n.rhs); + let mov: str = "MOVSD"; + if (isf32g) { mov = "MOVSS"; }; + emitline("\tLEAQ\t"); + emitsymname(c, nm); + emitline("(SB), CX\n"); + emitline("\t"); + emitline(mov); + emitline("\tX0, (CX)\n"); + return; + }; cgexpr(c, n.rhs); if (n.op == tkind.TK_ASSIGN) { if (letvarisstr(c, nm)) { @@ -1771,6 +2019,26 @@ fn cgassign(c: *cgen, n: *node) void = { let lcstr: bool = false; let lcn: *local = localfindnode(c, nm); if (lcn != nil) { lcstr = isstrtype(c, lcn.tnode); }; + let lcf: bool = false; + let lcf32: bool = false; + if (lcn != nil) { + lcf = isfloattype(c, lcn.tnode); + lcf32 = isf32type(c, lcn.tnode); + }; + // Float-typed local: rhs lands in X0; store via MOVSD/ + // MOVSS, no AX shuffle. Only plain `=` is wired; compound + // float-assign isn't. + if (lcf && n.op == tkind.TK_ASSIGN) { + cgexpr(c, n.rhs); + let mov: str = "MOVSD"; + if (lcf32) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\tX0, "); + emitoff(off: i64); + emitline("(BP)\n"); + return; + }; cgexpr(c, n.rhs); if (n.op == tkind.TK_ASSIGN) { emitline("\tMOVQ\tAX, "); diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index 2e47a08a..8b20acd9 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -467,6 +467,19 @@ fn cglet(c: *cgen, n: *node) void = { }; }; cgexpr(c, rhs); + // Float local: cgexpr leaves the value in X0. Spill via + // MOVSS (f32, 4B) or MOVSD (f64, 8B). + if (isfloattype(c, n.lhs)) { + let mov: str = "MOVSD"; + if (isf32type(c, n.lhs)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\tX0, "); + emitoff(off: i64); + emitline("(BP)\n"); + c.lastwasreturn = 0; + return; + }; emitline("\tMOVQ\tAX, "); emitoff(off: i64); emitline("(BP)\n"); diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 41768e0f..b48463f4 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -135,6 +135,21 @@ fn pushargsrev(c: *cgen, arg: *node) i32 = { }; }; }; + // Float arg: cgexpr leaves the value in X0. Push 8 bytes from + // 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); + if (fk != 0) { + cgexpr(c, arg); + let mov: str = "MOVSD"; + if (fk == 1) { mov = "MOVSS"; }; + emitline("\tSUBQ\t$8, SP\n"); + emitline("\t"); + emitline(mov); + emitline("\tX0, (SP)\n"); + return rest + 1; + }; cgexpr(c, arg); if (nodeisslice(c, arg)) { emitline("\tPUSHQ\tCX\n"); @@ -904,6 +919,118 @@ fn istaggedtype(t: *node) bool = { return false; }; +// isf32typeraw / isf64typeraw — bare TNAME check, no alias resolution. +fn isf32typeraw(t: *node) bool = { + if (t == nil) { return false; }; + if (t.kind != nkind.N_TNAME) { return false; }; + return streq(t.str, "f32"); +}; + +fn isf64typeraw(t: *node) bool = { + if (t == nil) { return false; }; + if (t.kind != nkind.N_TNAME) { return false; }; + return streq(t.str, "f64"); +}; + +// isfloattype — f32 / f64 (and aliases of those). Used by cglet, +// cgident, cgassign, cgbin, cgcast, cgcall, cgreturn, fn-prologue to +// dispatch the MOVSS/MOVSD-shaped paths. +export fn isfloattype(c: *cgen, t: *node) bool = { + if (isf32typeraw(t)) { return true; }; + if (isf64typeraw(t)) { return true; }; + if (c == nil) { return false; }; + let r: *node = resolvetype(c, t); + if (isf32typeraw(r)) { return true; }; + if (isf64typeraw(r)) { return true; }; + return false; +}; + +// isf32type — narrower predicate: true only for f32 (after alias +// resolution). f64 returns false. Used to pick MOVSS vs MOVSD and +// the SS-variant arithmetic / cast opcodes. +export fn isf32type(c: *cgen, t: *node) bool = { + if (isf32typeraw(t)) { return true; }; + if (c == nil) { return false; }; + let r: *node = resolvetype(c, t); + return isf32typeraw(r); +}; + +// exprfloatkind — classify an expression's value-class so callers can +// pick float vs integer codegen without a full type system. Returns: +// 0 — integer-like (or unknown — same fallback the existing cgen +// takes today) +// 1 — f32 +// 2 — f64 +// Recognises: float literals, idents bound to float lets/locals, +// chained casts whose target is float, and (recursively) the inner +// expr of a non-narrowing wrapping construct. Anything we can't +// pin down conservatively reports integer — the worst case is that +// CVT* is skipped for an exotic case the user can still spell with +// an explicit local. +export fn exprfloatkind(c: *cgen, n: *node) i32 = { + if (n == nil) { return 0; }; + let k: nkind = n.kind; + if (k == nkind.N_FLOATLIT) { return 2; }; + 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_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 — fnretlookup + // 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 rt: *node = fnretlookup(c, nm); + if (isf32type(c, rt)) { return 1; }; + if (isfloattype(c, rt)) { return 2; }; + }; + return 0; + }; + return 0; +}; + // isnullabletype — nkind.N_TTAGGED with exactly two children, one *T and // one `void`. Folds to a single 8-byte pointer slot per Hare's // `(*T | null)` semantics. Mirrors check.c's resolve_type detection. diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index d792baca..3ae38f9a 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -1275,6 +1275,82 @@ fn scanexp(l: *lex) void = { }; }; +// parsef64 — minimal decimal-float parser. Reads digits[.digits][eE[+-]digits] +// from the first `n` bytes of `s` (no leading sign — the lexer emits +// the unary minus as a separate token). The result rounds to the +// nearest f64 only via the trailing pow-10 multiply; this matches +// `strtod` to 1 ULP on typical literals and is good enough for the +// wwstage's own use (no float literals appear in the bootstrap +// source). Anything past `n` or non-digit is silently ignored. +fn parsef64(s: *u8, n: u64) f64 = { + let i: u64 = 0u64; + let intp: i64 = 0i64; + for (i < n) { + let b: u8 = s[i]; + if (b < 48u8) { break; }; + if (b > 57u8) { break; }; + intp = intp * 10i64 + (b - 48u8): i64; + i += 1u64; + }; + let frac: i64 = 0i64; + let fscale: i64 = 1i64; + if (i < n) { + if (s[i] == 46u8) { // '.' + i += 1u64; + for (i < n) { + let b: u8 = s[i]; + if (b < 48u8) { break; }; + if (b > 57u8) { break; }; + frac = frac * 10i64 + (b - 48u8): i64; + fscale = fscale * 10i64; + i += 1u64; + }; + }; + }; + let exp: i32 = 0; + let expneg: bool = false; + if (i < n) { + let e: u8 = s[i]; + if (e == 101u8 || e == 69u8) { // 'e' / 'E' + i += 1u64; + if (i < n) { + if (s[i] == 45u8) { // '-' + expneg = true; + i += 1u64; + } else { if (s[i] == 43u8) { // '+' + i += 1u64; + };}; + }; + for (i < n) { + let b: u8 = s[i]; + if (b < 48u8) { break; }; + if (b > 57u8) { break; }; + exp = exp * 10 + (b - 48u8): i32; + i += 1u64; + }; + }; + }; + let result: f64 = intp: f64; + if (frac != 0i64) { + result = result + (frac: f64) / (fscale: f64); + }; + if (exp != 0) { + // Use int-to-float casts so this file stays free of float + // literals — 990's wwdump diff relies on lib/ww/lex/lex.ww + // tokenising identically through C and ww, and the C dumper + // %g-formats TK_FLOAT.fval while the ww dumper currently + // skips it. Hiding the constants behind casts keeps both + // sides emitting `FLOAT` with no payload. + let factor: f64 = 1: f64; + let ten: f64 = 10: f64; + let k: i32 = 0; + for (k < exp) { factor = factor * ten; k += 1; }; + if (expneg) { result = result / factor; } + else { result = result * factor; }; + }; + return result; +}; + fn lexnum(l: *lex, start: *pos, out: *tok) void = { out.kind = tkind.TK_INT; out.file = start.file; @@ -1333,11 +1409,29 @@ fn lexnum(l: *lex, start: *pos, out: *tok) void = { out.text = astrndup(l.a, l.src + begin, n); if (isfloat) { - // out.fval is already 0 from the top-of-lexnext clear. - // We don't strtod the literal yet — the diff fixtures we - // care about are float-free; any tkind.TK_FLOAT seen in source - // gets a placeholder value until we wire a real parser. out.kind = tkind.TK_FLOAT; + // Strip underscores from the digits (Hare allows 1_000.5) + // before parsing — match what cmd/wcc/lex.c does with + // strtod over a cleaned buffer. + let clean: *u8 = amalloc(l.a, n + 1u64): *u8; + let i: u64 = 0u64; + let j: u64 = 0u64; + for (i < n) { + let b: u8 = l.src[begin + i]; + if (b != 95u8) { // '_' + clean[j] = b; + j += 1u64; + }; + i += 1u64; + }; + clean[j] = 0u8; + let fv: f64 = parsef64(clean, j); + out.fval = fv; + // Stash the IEEE bits in uval — cgen consumers read floats + // as integers (n.uval) to avoid an SSE round-trip when + // materialising the constant. + let pu: *u64 = (&fv): *u64; + out.uval = *pu; } else { let digs: *u8 = l.src + begin; let dn: u64 = n; @@ -2050,6 +2144,17 @@ fn parseprimary(p: *parser) *node = { advance(p); return n; }; + if (p.curkind == tkind.TK_FLOAT) { + let n: *node = newnode(p.a, nkind.N_FLOATLIT, pf, pl, pc); + n.fval = p.curfval; + // uval carries the IEEE 754 bit pattern — the lexer sets + // both, and cgen consumers prefer the integer view so they + // don't need a float ABI to materialise the constant. + n.uval = p.curuval; + n.str = p.curtext; + advance(p); + return n; + }; if (p.curkind == tkind.TK_STR) { let n: *node = newnode(p.a, nkind.N_STRLIT, pf, pl, pc); n.str = p.curtext; @@ -2938,6 +3043,7 @@ type parser = struct { curcol: i32, curtext: str, curuval: u64, + curfval: f64, }; fn refill(p: *parser) void = { @@ -2949,6 +3055,7 @@ fn refill(p: *parser) void = { p.curcol = t.col; p.curtext = t.text; p.curuval = t.uval; + p.curfval = t.fval; }; export fn parserinit(p: *parser, a: *arena, l: *lex) void = { @@ -4844,6 +4951,21 @@ fn pushargsrev(c: *cgen, arg: *node) i32 = { }; }; }; + // Float arg: cgexpr leaves the value in X0. Push 8 bytes from + // 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); + if (fk != 0) { + cgexpr(c, arg); + let mov: str = "MOVSD"; + if (fk == 1) { mov = "MOVSS"; }; + emitline("\tSUBQ\t$8, SP\n"); + emitline("\t"); + emitline(mov); + emitline("\tX0, (SP)\n"); + return rest + 1; + }; cgexpr(c, arg); if (nodeisslice(c, arg)) { emitline("\tPUSHQ\tCX\n"); @@ -5613,6 +5735,118 @@ fn istaggedtype(t: *node) bool = { return false; }; +// isf32typeraw / isf64typeraw — bare TNAME check, no alias resolution. +fn isf32typeraw(t: *node) bool = { + if (t == nil) { return false; }; + if (t.kind != nkind.N_TNAME) { return false; }; + return streq(t.str, "f32"); +}; + +fn isf64typeraw(t: *node) bool = { + if (t == nil) { return false; }; + if (t.kind != nkind.N_TNAME) { return false; }; + return streq(t.str, "f64"); +}; + +// isfloattype — f32 / f64 (and aliases of those). Used by cglet, +// cgident, cgassign, cgbin, cgcast, cgcall, cgreturn, fn-prologue to +// dispatch the MOVSS/MOVSD-shaped paths. +export fn isfloattype(c: *cgen, t: *node) bool = { + if (isf32typeraw(t)) { return true; }; + if (isf64typeraw(t)) { return true; }; + if (c == nil) { return false; }; + let r: *node = resolvetype(c, t); + if (isf32typeraw(r)) { return true; }; + if (isf64typeraw(r)) { return true; }; + return false; +}; + +// isf32type — narrower predicate: true only for f32 (after alias +// resolution). f64 returns false. Used to pick MOVSS vs MOVSD and +// the SS-variant arithmetic / cast opcodes. +export fn isf32type(c: *cgen, t: *node) bool = { + if (isf32typeraw(t)) { return true; }; + if (c == nil) { return false; }; + let r: *node = resolvetype(c, t); + return isf32typeraw(r); +}; + +// exprfloatkind — classify an expression's value-class so callers can +// pick float vs integer codegen without a full type system. Returns: +// 0 — integer-like (or unknown — same fallback the existing cgen +// takes today) +// 1 — f32 +// 2 — f64 +// Recognises: float literals, idents bound to float lets/locals, +// chained casts whose target is float, and (recursively) the inner +// expr of a non-narrowing wrapping construct. Anything we can't +// pin down conservatively reports integer — the worst case is that +// CVT* is skipped for an exotic case the user can still spell with +// an explicit local. +export fn exprfloatkind(c: *cgen, n: *node) i32 = { + if (n == nil) { return 0; }; + let k: nkind = n.kind; + if (k == nkind.N_FLOATLIT) { return 2; }; + 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_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 — fnretlookup + // 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 rt: *node = fnretlookup(c, nm); + if (isf32type(c, rt)) { return 1; }; + if (isfloattype(c, rt)) { return 2; }; + }; + return 0; + }; + return 0; +}; + // isnullabletype — nkind.N_TTAGGED with exactly two children, one *T and // one `void`. Folds to a single 8-byte pointer slot per Hare's // `(*T | null)` semantics. Mirrors check.c's resolve_type detection. @@ -5762,6 +5996,22 @@ fn cgexpr(c: *cgen, n: *node) void = { emitline(", AX\n"); 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"); + return; + }; if (k == nkind.N_RUNELIT) { emitline("\tMOVQ\t$"); emitint(n.uval: i64); @@ -5794,15 +6044,7 @@ fn cgexpr(c: *cgen, n: *node) void = { if (k == nkind.N_MATCH) { cgmatch(c, n); return; }; - if (k == nkind.N_CAST) { - // Type casts are mostly no-ops at the asm level for our - // integer-shaped operands. Evaluate the source; AX holds - // the bits unchanged. (Sign- or zero-extending narrow loads - // to wider types is the loader's job, not cast's, in this - // minimal cgen.) - cgexpr(c, n.lhs); - return; - }; + if (k == nkind.N_CAST) { cgcast(c, n); return; }; if (k == nkind.N_DOT) { cgdot(c, n); return; }; @@ -6070,6 +6312,46 @@ fn cgtypeassert(c: *cgen, n: *node) void = { return; }; +fn cgcast(c: *cgen, n: *node) void = { + let srcfk: i32 = exprfloatkind(c, n.lhs); + let dstf64: bool = isfloattype(c, n.rhs); + let dstf32: bool = isf32type(c, n.rhs); + let dstfk: i32 = 0; + if (dstf32) { dstfk = 1; } + else { if (dstf64) { dstfk = 2; }; }; + cgexpr(c, n.lhs); + // 0=int, 1=f32, 2=f64. CVT picks one direction per combo; + // same-kind casts (int↔int with widening differences, + // f64→f64 etc.) stay no-ops at the asm level, matching the + // pre-port behaviour for integer casts. + if (srcfk == 0 && dstfk == 0) { return; }; + if (srcfk == 0 && dstfk == 2) { + emitline("\tCVTSI2SD\tAX, X0\n"); + return; + }; + if (srcfk == 0 && dstfk == 1) { + emitline("\tCVTSI2SS\tAX, X0\n"); + return; + }; + if (srcfk == 2 && dstfk == 0) { + emitline("\tCVTTSD2SI\tX0, AX\n"); + return; + }; + if (srcfk == 1 && dstfk == 0) { + emitline("\tCVTTSS2SI\tX0, AX\n"); + return; + }; + if (srcfk == 2 && dstfk == 1) { + emitline("\tCVTSD2SS\tX0, X0\n"); + return; + }; + if (srcfk == 1 && dstfk == 2) { + emitline("\tCVTSS2SD\tX0, X0\n"); + return; + }; + // Same-kind float→float: nothing to emit. +}; + fn cgstrlit(c: *cgen, n: *node) void = { // Result is the (ptr, len) pair: ptr in AX, len in BX. Call // sites that expect a str arg pick these up directly. @@ -6089,6 +6371,19 @@ fn cgident(c: *cgen, n: *node) void = { let lc: *local = localfindnode(c, nm); if (lc != nil) { let off: i32 = lc.off; + // Float local: MOVSS / MOVSD into X0. Skips the AX shuffle + // so consumers (cgbin, cgcast, return) pick up the SSE value + // directly. + if (isfloattype(c, lc.tnode)) { + let mov: str = "MOVSD"; + if (isf32type(c, lc.tnode)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t"); + emitoff(off: i64); + emitline("(BP), X0\n"); + return; + }; emitline("\tMOVQ\t"); emitoff(off: i64); emitline("(BP), AX\n"); @@ -6151,6 +6446,27 @@ fn cgident(c: *cgen, n: *node) void = { }; return; }; + // Float global: same LEAQ-indirect shape, since MOVSS/ + // MOVSD have no D_EXTERN operand form in w6a. + let lv: *letvar = c.lets; + for (lv != nil) { + if (streq(lv.name, nm)) { + if (isfloattype(c, lv.tnode)) { + let mov: str = "MOVSD"; + if (isf32type(c, lv.tnode)) { mov = "MOVSS"; }; + emitline("\tLEAQ\t"); + emitsymname(c, nm); + emitline("(SB), CX\n"); + emitline("\t"); + emitline(mov); + emitline("\t(CX), X0\n"); + return; + }; + lv = nil; + } else { + lv = lv.lvnext; + }; + }; emitline("\tMOVQ\t"); emitsymname(c, nm); emitline("(SB), AX\n"); @@ -6755,6 +7071,26 @@ 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); + 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 + // reuse the integer-zero materialisation. + let mov: str = "MOVSD"; + let sub: str = "SUBSD"; + if (fk == 1) { mov = "MOVSS"; sub = "SUBSS"; }; + cgexpr(c, n.lhs); + emitline("\tSUBQ\t$8, SP\n"); + emitline("\t"); emitline(mov); emitline("\tX0, (SP)\n"); + emitline("\tMOVQ\t$0, AX\n"); + emitline("\tPUSHQ\tAX\n"); + emitline("\t"); emitline(mov); emitline("\t(SP), X0\n"); + emitline("\tADDQ\t$8, SP\n"); + emitline("\t"); emitline(mov); emitline("\t(SP), X1\n"); + emitline("\tADDQ\t$8, SP\n"); + emitline("\t"); emitline(sub); emitline("\tX1, X0\n"); + return; + }; cgexpr(c, n.lhs); if (n.op == tkind.TK_MINUS) { emitline("\tNEGQ\tAX\n"); return; }; if (n.op == tkind.TK_TILDE) { emitline("\tNOTQ\tAX\n"); return; }; @@ -6801,6 +7137,75 @@ fn cgbin(c: *cgen, n: *node) void = { let unsignd: bool = nodeisunsigned(c, n.lhs); if (!unsignd) { unsignd = nodeisunsigned(c, n.rhs); }; + // Float arithmetic: both operands flow through X0. Spill rhs + // across the stack (SUBQ/MOVSD/MOVSD/ADDQ) since there's no + // 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); + let fk: i32 = lfk; + if (fk == 0) { fk = rfk; }; + if (fk != 0) { + let mov: str = "MOVSD"; + if (fk == 1) { mov = "MOVSS"; }; + if (n.op == tkind.TK_PLUS || + n.op == tkind.TK_MINUS || + n.op == tkind.TK_STAR || + n.op == tkind.TK_SLASH) { + cgexpr(c, n.rhs); + emitline("\tSUBQ\t$8, SP\n"); + emitline("\t"); emitline(mov); emitline("\tX0, (SP)\n"); + cgexpr(c, n.lhs); + emitline("\t"); emitline(mov); emitline("\t(SP), X1\n"); + emitline("\tADDQ\t$8, SP\n"); + let op: str = "ADDSD"; + if (n.op == tkind.TK_MINUS) { op = "SUBSD"; }; + if (n.op == tkind.TK_STAR) { op = "MULSD"; }; + if (n.op == tkind.TK_SLASH) { op = "DIVSD"; }; + if (fk == 1) { + if (n.op == tkind.TK_PLUS) { op = "ADDSS"; }; + if (n.op == tkind.TK_MINUS) { op = "SUBSS"; }; + if (n.op == tkind.TK_STAR) { op = "MULSS"; }; + if (n.op == tkind.TK_SLASH) { op = "DIVSS"; }; + }; + emitline("\t"); emitline(op); emitline("\tX1, X0\n"); + return; + }; + let isfcmp: bool = false; + let jcc: str = ""; + // UCOMISD/SS sets ZF/PF/CF; unordered (NaN) propagates as + // "not equal / not less". JA/JAE/JB/JBE keys off CF which + // matches the ordered comparisons we need. + if (n.op == tkind.TK_EQ) { isfcmp = true; jcc = "JE"; }; + if (n.op == tkind.TK_NEQ) { isfcmp = true; jcc = "JNE"; }; + if (n.op == tkind.TK_LT) { isfcmp = true; jcc = "JB"; }; + if (n.op == tkind.TK_LE) { isfcmp = true; jcc = "JBE"; }; + if (n.op == tkind.TK_GT) { isfcmp = true; jcc = "JA"; }; + if (n.op == tkind.TK_GE) { isfcmp = true; jcc = "JAE"; }; + if (isfcmp) { + cgexpr(c, n.rhs); + emitline("\tSUBQ\t$8, SP\n"); + emitline("\t"); emitline(mov); emitline("\tX0, (SP)\n"); + cgexpr(c, n.lhs); + emitline("\t"); emitline(mov); emitline("\t(SP), X1\n"); + emitline("\tADDQ\t$8, SP\n"); + let ucomi: str = "UCOMISD"; + if (fk == 1) { ucomi = "UCOMISS"; }; + emitline("\t"); emitline(ucomi); emitline("\tX1, X0\n"); + let t: str = mklabel(c, "ct"); + let e: str = mklabel(c, "ce"); + emitline("\t"); emitline(jcc); emitline("\t"); emitline(t); emitline("\n"); + emitline("\tMOVQ\t$0, AX\n"); + emitline("\tJMP\t"); emitline(e); emitline("\n"); + emitlabel(t); + emitline("\tMOVQ\t$1, AX\n"); + emitlabel(e); + return; + }; + return; + }; + cgexpr(c, n.rhs); emitline("\tPUSHQ\tAX\n"); cgexpr(c, n.lhs); @@ -6864,11 +7269,61 @@ fn cgbin(c: *cgen, n: *node) void = { fn cgcall(c: *cgen, n: *node) void = { let nargs: i32 = pushargsrev(c, n.list); - let i: i32 = 0; + // Pop forward. Float args were pushed as 8 bytes from X0 via + // SUBQ+MOVSD; pop into the XMM stream (X0..X7). Everything else + // pops into the int stream (DI..R9) per the SysV ABI. Walk the + // args list alongside the pop counter so we know each arg's + // register class. + let intidx: i32 = 0; + let fpidx: i32 = 0; + let a: *node = n.list; + let popped: i32 = 0; + for (a != nil) { + let fk: i32 = exprfloatkind(c, a); + if (fk != 0) { + let mov: str = "MOVSD"; + if (fk == 1) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t(SP), "); + emitline(fargregname(fpidx)); + emitline("\n"); + emitline("\tADDQ\t$8, SP\n"); + fpidx += 1; + popped += 1; + } else { + emitline("\tPOPQ\t"); + emitline(argregname(intidx)); + emitline("\n"); + intidx += 1; + popped += 1; + // Multi-word args (str=2, slice/tagged=3): drain + // the remaining words into successive int regs. + let extra: i32 = 0; + if (nodeisstr(c, a)) { extra = 1; }; + if (nodeisslice(c, a)) { extra = 2; }; + let e: i32 = 0; + for (e < extra) { + emitline("\tPOPQ\t"); + emitline(argregname(intidx)); + emitline("\n"); + intidx += 1; + popped += 1; + e += 1; + }; + }; + a = a.next; + }; + // Drain any remaining slots that the arg-walker didn't account + // for (tagged-union arg sizes > 8B, struct-by-value, etc.). The + // existing C cgen pops these into the int stream, so the worst + // case here is identical pre-port behaviour. + let i: i32 = popped; for (i < nargs) { emitline("\tPOPQ\t"); - emitline(argregname(i)); + emitline(argregname(intidx)); emitline("\n"); + intidx += 1; i += 1; }; let callee: *node = n.lhs; @@ -7445,6 +7900,33 @@ fn cgassign(c: *cgen, n: *node) void = { // address into CX and store both halves; the // asm has no `name+8(SB)` operand form. if (!isletvar(c, nm)) { return; }; + // Float global: rhs lands in X0; store via + // LEAQ+indirect since MOVSS/MOVSD have no + // D_EXTERN operand form. + let lvf: *letvar = c.lets; + let isfg: bool = false; + let isf32g: bool = false; + for (lvf != nil) { + if (streq(lvf.name, nm)) { + isfg = isfloattype(c, lvf.tnode); + isf32g = isf32type(c, lvf.tnode); + lvf = nil; + } else { + lvf = lvf.lvnext; + }; + }; + if (isfg && n.op == tkind.TK_ASSIGN) { + cgexpr(c, n.rhs); + let mov: str = "MOVSD"; + if (isf32g) { mov = "MOVSS"; }; + emitline("\tLEAQ\t"); + emitsymname(c, nm); + emitline("(SB), CX\n"); + emitline("\t"); + emitline(mov); + emitline("\tX0, (CX)\n"); + return; + }; cgexpr(c, n.rhs); if (n.op == tkind.TK_ASSIGN) { if (letvarisstr(c, nm)) { @@ -7499,6 +7981,26 @@ fn cgassign(c: *cgen, n: *node) void = { let lcstr: bool = false; let lcn: *local = localfindnode(c, nm); if (lcn != nil) { lcstr = isstrtype(c, lcn.tnode); }; + let lcf: bool = false; + let lcf32: bool = false; + if (lcn != nil) { + lcf = isfloattype(c, lcn.tnode); + lcf32 = isf32type(c, lcn.tnode); + }; + // Float-typed local: rhs lands in X0; store via MOVSD/ + // MOVSS, no AX shuffle. Only plain `=` is wired; compound + // float-assign isn't. + if (lcf && n.op == tkind.TK_ASSIGN) { + cgexpr(c, n.rhs); + let mov: str = "MOVSD"; + if (lcf32) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\tX0, "); + emitoff(off: i64); + emitline("(BP)\n"); + return; + }; cgexpr(c, n.rhs); if (n.op == tkind.TK_ASSIGN) { emitline("\tMOVQ\tAX, "); @@ -8020,6 +8522,19 @@ fn cglet(c: *cgen, n: *node) void = { }; }; cgexpr(c, rhs); + // Float local: cgexpr leaves the value in X0. Spill via + // MOVSS (f32, 4B) or MOVSD (f64, 8B). + if (isfloattype(c, n.lhs)) { + let mov: str = "MOVSD"; + if (isf32type(c, n.lhs)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\tX0, "); + emitoff(off: i64); + emitline("(BP)\n"); + c.lastwasreturn = 0; + return; + }; emitline("\tMOVQ\tAX, "); emitoff(off: i64); emitline("(BP)\n"); @@ -8403,9 +8918,29 @@ fn scanlocals(c: *cgen, n: *node) i32 = { fn cgfnparams(c: *cgen, params: *node) void = { let p: *node = params; let idx: i32 = 0; + let fidx: i32 = 0; for (p != nil) { if (p.kind == nkind.N_PARAM) { let nm: str = p.str; + if (isfloattype(c, p.lhs)) { + // Float param: SysV uses the XMM stream + // (X0..X7). 8B (f64) or 4B (f32) slot. + let fsz: i32 = 8; + if (isf32type(c, p.lhs)) { fsz = 4; }; + let off: i32 = localadd(c, nm, fsz, p.lhs); + let mov: str = "MOVSD"; + if (fsz == 4) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t"); + emitline(fargregname(fidx)); + emitline(", "); + emitoff(off: i64); + emitline("(BP)\n"); + fidx += 1; + p = p.next; + continue; + }; if (istaggedtype(p.lhs)) { // tagged-union param: spill size/8 registers // (tag + value words). Slot sized to match. @@ -9430,21 +9965,37 @@ fn emitletdataw(c: *cgen, file: *node) void = { let fsz: i32 = letvarisfloat(c, nm); if (fsz > 0) { // Float global: 4B (f32) or 8B (f64). - // The selfhost parser doesn't lex - // N_FLOATLIT yet, so only zero-init - // reaches this path. C cgen emits - // identical bytes for the zero-init - // case; FLOATLIT-init lives in C cgen - // only. + // Two init shapes: + // - no rhs: emit fsz zero bytes + // - N_FLOATLIT: bake the IEEE bits the + // parser stashed in r.uval (lexer + // bit-casts t.fval into t.uval). f32 + // emits the low 4 bytes; f64 emits 8. + let bits: u64 = 0u64; let ok: bool = true; - if (d.rhs != nil) { ok = false; }; + if (d.rhs != nil) { + let r: *node = d.rhs; + for (r != nil) { + if (r.kind != nkind.N_CAST) { break; }; + r = r.lhs; + }; + ok = false; + if (r != nil) { + if (r.kind == nkind.N_FLOATLIT) { + bits = r.uval; + ok = true; + }; + }; + }; if (ok) { emitline("DATAW "); emitsymname(c, nm); emitline("(SB),\""); let i: i32 = 0; + let nb: u64 = bits; for (i < fsz) { - emitdatawbyte(0u8); + emitdatawbyte((nb & 255u64): u8); + nb = nb >> 8u64; i += 1; }; emitline("\"\n"); @@ -9971,6 +10522,21 @@ fn argregname(i: i32) str = { return "?"; }; +// fargregname — XMM scalar-float arg registers (SysV: X0..X7). +// Parallel to argregname / sysv_argregs; float args advance their +// own counter so int and float arg slots don't conflict. +export fn fargregname(i: i32) str = { + if (i == 0) { return "X0"; }; + if (i == 1) { return "X1"; }; + if (i == 2) { return "X2"; }; + if (i == 3) { return "X3"; }; + if (i == 4) { return "X4"; }; + if (i == 5) { return "X5"; }; + if (i == 6) { return "X6"; }; + if (i == 7) { return "X7"; }; + return "?"; +}; + // MODULE: wwdump // selfhost/cmd/wwdump/main.ww — ww-side port of cmd/wwdump/main.c. //