selfhost: port float lex + expression cgen — feature parity with C

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.
This commit is contained in:
2026-05-12 14:21:50 +09:00
parent a9b804935c
commit 5155ba55f3
10 changed files with 1767 additions and 69 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 = {

View File

@@ -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.
//

View File

@@ -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 "?";
};

View File

@@ -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.

View File

@@ -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, ");

View File

@@ -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");

View File

@@ -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.

View File

@@ -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.
//