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:
@@ -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, ");
|
||||
|
||||
Reference in New Issue
Block a user