diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 6b194993..cfb23d2e 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -398,8 +398,8 @@ struct LetVar { static LetVar *letvars; /* Slot size for a top-level `let` of type t, or 0 if the type isn't - * supported as a writable global yet. Floats (MOVSS/SD) and tagged - * unions are deferred. enums route through their storage type. + * supported as a writable global yet. Tagged unions are deferred. + * enums route through their storage type. * Keep this tight — extending it requires the matching load/store * code below. */ static int @@ -415,6 +415,10 @@ let_emit_size(Type *t) case TY_INT: case TY_UINT: case TY_UINTPTR: case TY_PTR: return 8; + case TY_F32: + return 4; /* MOVSS loads/stores 4B via LEAQ+indir. */ + case TY_F64: + return 8; /* MOVSD loads/stores 8B via LEAQ+indir. */ case TY_STR: return 16; /* {ptr, len}; literal-strlit init NYI. */ case TY_SLICE: @@ -459,6 +463,17 @@ let_isstruct(Type *t) return u && u->kind == TY_STRUCT; } +/* Is the unwrapped type a float (f32 or f64)? Float globals flow + * through X0 — load/store goes LEAQ name(SB),CX → MOVSS/MOVSD via the + * indirect, since the asm has no D_EXTERN form for SSE moves yet. */ +static int +let_isfloat(Type *t) +{ + if (t == NULL) return 0; + Type *u = (t->kind == TY_NAMED) ? t->under : t; + return u && (u->kind == TY_F32 || u->kind == TY_F64); +} + /* Returns the unwrapped Type — handy when we need to walk struct * fields. NULL if t is NULL or unresolved. */ static Type * @@ -815,6 +830,15 @@ cgexpr(Cg *c, Node *n, Local *locals) ins2(c, A_MOVQ, amem(D_CX, 16), areg(D_CX)); goto ident_done; } + if (let_islet(n->str) && let_isfloat(n->type)) { + /* Top-level float global: same LEAQ-indirect + * shape as str/slice, since MOVSS/MOVSD have + * no D_EXTERN operand form in w6a. */ + int op = type_isf32(n->type) ? A_MOVSS : A_MOVSD; + ins2(c, A_LEAQ, masym(c, n->str), areg(D_CX)); + ins2(c, op, amem(D_CX, 0), areg(D_X0)); + goto ident_done; + } ins2(c, A_MOVQ, masym(c, n->str), areg(D_AX)); } ident_done: @@ -1363,13 +1387,17 @@ cgexpr(Cg *c, Node *n, Local *locals) } } } - /* float assignment to a local */ + /* float assignment to a local or top-level global. Globals + * route through LEAQ+indirect (no D_EXTERN SSE in w6a). */ if (n->lhs && n->lhs->kind == N_IDENT && node_isfloat(n)) { cgexpr(c, n->rhs, locals); /* X0 */ + int op = op_for(n, A_MOVSD, A_MOVSS); int off = localfind(locals, n->lhs->str); if (off != 0) { - int op = op_for(n, A_MOVSD, A_MOVSS); ins2(c, op, areg(D_X0), amem(D_BP, off)); + } else if (let_islet(n->lhs->str)) { + ins2(c, A_LEAQ, masym(c, n->lhs->str), areg(D_CX)); + ins2(c, op, areg(D_X0), amem(D_CX, 0)); } break; } @@ -3745,6 +3773,31 @@ emit_lets(Cg *c, FILE *out, Node *file) if (d->str == NULL || d->str[0] == '\0') continue; int sz = let_emit_size(d->type); if (sz == 0) continue; + if (let_isfloat(d->type)) { + /* sz is 4 (f32) or 8 (f64). FLOATLIT init or zero. */ + int isf32 = type_isf32(d->type); + u64 v = 0; + if (d->rhs != NULL) { + Node *r = d->rhs; + while (r != NULL && r->kind == N_CAST) r = r->lhs; + if (r == NULL) continue; + if (r->kind != N_FLOATLIT) continue; + if (isf32) { + union { float f; u32 u; } x; + x.f = (float)r->fval; + v = (u64)x.u; + } else { + union { double d; u64 u; } x; + x.d = r->fval; + v = x.u; + } + } + fprintf(out, "DATAW %s(SB),\"", mod_mangle(c, d->str)); + for (int i = 0; i < sz; i++) + emit_data_byte(out, (u8)((v >> (i * 8)) & 0xff)); + fputs("\"\n", out); + continue; + } if (sz == 8) { u64 v = 0; if (d->rhs != NULL) { diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 6c49e1ea..493733fd 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -9147,8 +9147,8 @@ fn internstrlit(c: *cgen, bytes: str) str = { // letscalarprim — recognise the bare type-name keywords whose values // fit in an 8-byte .data slot and load back with a plain MOVQ. Float -// types deliberately excluded; they need MOVSS/MOVSD. Mirrors C -// `let_emit_size`'s 8-byte branch. +// types are handled separately by letfloatprim — they need MOVSS/MOVSD +// and use 4-byte (f32) or 8-byte (f64) slots. fn letscalarprim(nm: str) bool = { if (streq(nm, "bool")) { return true; }; if (streq(nm, "rune")) { return true; }; @@ -9166,10 +9166,19 @@ fn letscalarprim(nm: str) bool = { return false; }; +// letfloatprim — float type-name keywords. f32 → 4B slot, f64 → 8B. +// Returns the slot size or 0 if not a float type. +fn letfloatprim(nm: str) i32 = { + if (streq(nm, "f32")) { return 4; }; + if (streq(nm, "f64")) { return 8; }; + return 0; +}; + // letemitsize — slot size in bytes for a top-level `let`, or 0 if // the type isn't yet supported as a writable global. Walks type // aliases so byte output matches C cgen, which resolves Type kinds. -// 8 → scalar (literal init supported) +// 4 → f32 (literal init supported) +// 8 → scalar or f64 (literal init supported) // 16 → str (only zero-init / nil / "" supported) // 24 → slice (only zero-init supported) // varies → struct (zero-init only; field reads/scalar-field writes) @@ -9182,6 +9191,8 @@ fn letemitsize(c: *cgen, d: *node) i32 = { if (t.kind != nkind.N_TNAME) { return 0; }; let nm: str = t.str; if (letscalarprim(nm)) { return 8; }; + let fsz: i32 = letfloatprim(nm); + if (fsz > 0) { return fsz; }; if (streq(nm, "str")) { return 16; }; let si: *structinfo = structlookup(c, nm); if (si != nil) { return si.totsize; }; @@ -9264,6 +9275,29 @@ fn letvarisslice(c: *cgen, name: str) bool = { return false; }; +// letvarisfloat — slot size for a named float global, or 0 if not +// a float-typed let. Walks aliases so the byte-identity contract +// matches C cgen's `let_isfloat` (which resolves Type kinds). +fn letvarisfloat(c: *cgen, name: str) i32 = { + let lv: *letvar = c.lets; + for (lv != nil) { + if (streq(lv.name, name)) { + let t: *node = lv.tnode; + for (t != nil) { + if (t.kind != nkind.N_TNAME) { return 0; }; + let fsz: i32 = letfloatprim(t.str); + if (fsz > 0) { return fsz; }; + let nx: *node = aliaslookup(c, t.str); + if (nx == nil) { return 0; }; + t = nx; + }; + return 0; + }; + lv = lv.lvnext; + }; + return 0; +}; + // letvarisstruct — is the named top-level let a struct global? // Struct globals use LEAQ name(SB), CX as the field-access base; the // cgdot read and cgassign write paths branch on this to skip the @@ -9393,7 +9427,30 @@ fn emitletdataw(c: *cgen, file: *node) void = { if (nm.len > 0) { let sz: i32 = letemitsize(c, d); let issg: bool = letvarisstruct(c, nm); - if (sz == 8 && !issg) { + 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. + let ok: bool = true; + if (d.rhs != nil) { ok = false; }; + if (ok) { + emitline("DATAW "); + emitsymname(c, nm); + emitline("(SB),\""); + let i: i32 = 0; + for (i < fsz) { + emitdatawbyte(0u8); + i += 1; + }; + emitline("\"\n"); + }; + }; + if (sz == 8 && !issg && fsz == 0) { let v: u64 = 0u64; let ok: bool = true; if (d.rhs != nil) { diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index ca96cd84..e02ec474 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -562,8 +562,8 @@ fn internstrlit(c: *cgen, bytes: str) str = { // letscalarprim — recognise the bare type-name keywords whose values // fit in an 8-byte .data slot and load back with a plain MOVQ. Float -// types deliberately excluded; they need MOVSS/MOVSD. Mirrors C -// `let_emit_size`'s 8-byte branch. +// types are handled separately by letfloatprim — they need MOVSS/MOVSD +// and use 4-byte (f32) or 8-byte (f64) slots. fn letscalarprim(nm: str) bool = { if (streq(nm, "bool")) { return true; }; if (streq(nm, "rune")) { return true; }; @@ -581,10 +581,19 @@ fn letscalarprim(nm: str) bool = { return false; }; +// letfloatprim — float type-name keywords. f32 → 4B slot, f64 → 8B. +// Returns the slot size or 0 if not a float type. +fn letfloatprim(nm: str) i32 = { + if (streq(nm, "f32")) { return 4; }; + if (streq(nm, "f64")) { return 8; }; + return 0; +}; + // letemitsize — slot size in bytes for a top-level `let`, or 0 if // the type isn't yet supported as a writable global. Walks type // aliases so byte output matches C cgen, which resolves Type kinds. -// 8 → scalar (literal init supported) +// 4 → f32 (literal init supported) +// 8 → scalar or f64 (literal init supported) // 16 → str (only zero-init / nil / "" supported) // 24 → slice (only zero-init supported) // varies → struct (zero-init only; field reads/scalar-field writes) @@ -597,6 +606,8 @@ fn letemitsize(c: *cgen, d: *node) i32 = { if (t.kind != nkind.N_TNAME) { return 0; }; let nm: str = t.str; if (letscalarprim(nm)) { return 8; }; + let fsz: i32 = letfloatprim(nm); + if (fsz > 0) { return fsz; }; if (streq(nm, "str")) { return 16; }; let si: *structinfo = structlookup(c, nm); if (si != nil) { return si.totsize; }; @@ -679,6 +690,29 @@ fn letvarisslice(c: *cgen, name: str) bool = { return false; }; +// letvarisfloat — slot size for a named float global, or 0 if not +// a float-typed let. Walks aliases so the byte-identity contract +// matches C cgen's `let_isfloat` (which resolves Type kinds). +fn letvarisfloat(c: *cgen, name: str) i32 = { + let lv: *letvar = c.lets; + for (lv != nil) { + if (streq(lv.name, name)) { + let t: *node = lv.tnode; + for (t != nil) { + if (t.kind != nkind.N_TNAME) { return 0; }; + let fsz: i32 = letfloatprim(t.str); + if (fsz > 0) { return fsz; }; + let nx: *node = aliaslookup(c, t.str); + if (nx == nil) { return 0; }; + t = nx; + }; + return 0; + }; + lv = lv.lvnext; + }; + return 0; +}; + // letvarisstruct — is the named top-level let a struct global? // Struct globals use LEAQ name(SB), CX as the field-access base; the // cgdot read and cgassign write paths branch on this to skip the @@ -808,7 +842,30 @@ fn emitletdataw(c: *cgen, file: *node) void = { if (nm.len > 0) { let sz: i32 = letemitsize(c, d); let issg: bool = letvarisstruct(c, nm); - if (sz == 8 && !issg) { + 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. + let ok: bool = true; + if (d.rhs != nil) { ok = false; }; + if (ok) { + emitline("DATAW "); + emitsymname(c, nm); + emitline("(SB),\""); + let i: i32 = 0; + for (i < fsz) { + emitdatawbyte(0u8); + i += 1; + }; + emitline("\"\n"); + }; + }; + if (sz == 8 && !issg && fsz == 0) { let v: u64 = 0u64; let ok: bool = true; if (d.rhs != nil) { diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 5f1a460a..d792baca 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -9147,8 +9147,8 @@ fn internstrlit(c: *cgen, bytes: str) str = { // letscalarprim — recognise the bare type-name keywords whose values // fit in an 8-byte .data slot and load back with a plain MOVQ. Float -// types deliberately excluded; they need MOVSS/MOVSD. Mirrors C -// `let_emit_size`'s 8-byte branch. +// types are handled separately by letfloatprim — they need MOVSS/MOVSD +// and use 4-byte (f32) or 8-byte (f64) slots. fn letscalarprim(nm: str) bool = { if (streq(nm, "bool")) { return true; }; if (streq(nm, "rune")) { return true; }; @@ -9166,10 +9166,19 @@ fn letscalarprim(nm: str) bool = { return false; }; +// letfloatprim — float type-name keywords. f32 → 4B slot, f64 → 8B. +// Returns the slot size or 0 if not a float type. +fn letfloatprim(nm: str) i32 = { + if (streq(nm, "f32")) { return 4; }; + if (streq(nm, "f64")) { return 8; }; + return 0; +}; + // letemitsize — slot size in bytes for a top-level `let`, or 0 if // the type isn't yet supported as a writable global. Walks type // aliases so byte output matches C cgen, which resolves Type kinds. -// 8 → scalar (literal init supported) +// 4 → f32 (literal init supported) +// 8 → scalar or f64 (literal init supported) // 16 → str (only zero-init / nil / "" supported) // 24 → slice (only zero-init supported) // varies → struct (zero-init only; field reads/scalar-field writes) @@ -9182,6 +9191,8 @@ fn letemitsize(c: *cgen, d: *node) i32 = { if (t.kind != nkind.N_TNAME) { return 0; }; let nm: str = t.str; if (letscalarprim(nm)) { return 8; }; + let fsz: i32 = letfloatprim(nm); + if (fsz > 0) { return fsz; }; if (streq(nm, "str")) { return 16; }; let si: *structinfo = structlookup(c, nm); if (si != nil) { return si.totsize; }; @@ -9264,6 +9275,29 @@ fn letvarisslice(c: *cgen, name: str) bool = { return false; }; +// letvarisfloat — slot size for a named float global, or 0 if not +// a float-typed let. Walks aliases so the byte-identity contract +// matches C cgen's `let_isfloat` (which resolves Type kinds). +fn letvarisfloat(c: *cgen, name: str) i32 = { + let lv: *letvar = c.lets; + for (lv != nil) { + if (streq(lv.name, name)) { + let t: *node = lv.tnode; + for (t != nil) { + if (t.kind != nkind.N_TNAME) { return 0; }; + let fsz: i32 = letfloatprim(t.str); + if (fsz > 0) { return fsz; }; + let nx: *node = aliaslookup(c, t.str); + if (nx == nil) { return 0; }; + t = nx; + }; + return 0; + }; + lv = lv.lvnext; + }; + return 0; +}; + // letvarisstruct — is the named top-level let a struct global? // Struct globals use LEAQ name(SB), CX as the field-access base; the // cgdot read and cgassign write paths branch on this to skip the @@ -9393,7 +9427,30 @@ fn emitletdataw(c: *cgen, file: *node) void = { if (nm.len > 0) { let sz: i32 = letemitsize(c, d); let issg: bool = letvarisstruct(c, nm); - if (sz == 8 && !issg) { + 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. + let ok: bool = true; + if (d.rhs != nil) { ok = false; }; + if (ok) { + emitline("DATAW "); + emitsymname(c, nm); + emitline("(SB),\""); + let i: i32 = 0; + for (i < fsz) { + emitdatawbyte(0u8); + i += 1; + }; + emitline("\"\n"); + }; + }; + if (sz == 8 && !issg && fsz == 0) { let v: u64 = 0u64; let ok: bool = true; if (d.rhs != nil) { diff --git a/test/wcc/630_let_global.c b/test/wcc/630_let_global.c index ac043cdb..6397d9fc 100644 --- a/test/wcc/630_let_global.c +++ b/test/wcc/630_let_global.c @@ -188,6 +188,50 @@ static const struct fixture fixtures[] = { "};\n", 42, }, + { + "f64-literal-init", + /* `let pi: f64 = 3.14;` — DATAW bakes the 8 LE bytes of + * the double directly. Read goes LEAQ name(SB),CX + + * MOVSD (CX),X0; cast truncates to i32. */ + "let pi: f64 = 7.5;\n" + "fn main() i32 = { return pi: i32; };\n", + 7, + }, + { + "f32-literal-init", + /* f32 globals are 4-byte slots; literal init bakes the + * single-precision bit pattern. */ + "let half: f32 = 4.25;\n" + "fn main() i32 = { return half: i32; };\n", + 4, + }, + { + "f64-zero-init", + "let z: f64;\n" + "fn main() i32 = { return z: i32; };\n", + 0, + }, + { + "f64-reassign", + /* Reassigning an f64 global goes LEAQ name(SB),CX + + * MOVSD X0,(CX); read-back through the same shape. */ + "let pi: f64 = 1.0;\n" + "fn main() i32 = {\n" + "\tpi = 7.5;\n" + "\treturn pi: i32;\n" + "};\n", + 7, + }, + { + "f64-arith", + "let a: f64 = 7.5;\n" + "let b: f64 = 4.25;\n" + "fn main() i32 = {\n" + "\tlet s: f64 = a + b;\n" + "\treturn s: i32;\n" + "};\n", + 11, + }, { "struct-narrow-field", /* u8 field on a struct global. Read uses MOVZBQ, store