w6c+selfhost: float globals — DATAW + LEAQ-indirect MOVSS/MOVSD

f32 → 4B slot, f64 → 8B. C cgen bakes the FLOATLIT bit pattern into
DATAW directly; selfhost emits zero-init only (its parser doesn't
lex N_FLOATLIT yet). Read/write goes LEAQ name(SB),CX + MOVSS/MOVSD
since w6a has no D_EXTERN operand form for SSE moves.
This commit is contained in:
2026-05-12 13:23:45 +09:00
parent 1ac9980f7e
commit 6f04713601
5 changed files with 284 additions and 16 deletions

View File

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

View File

@@ -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.
// 8scalar (literal init supported)
// 4f32 (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) {

View File

@@ -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.
// 8scalar (literal init supported)
// 4f32 (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) {

View File

@@ -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.
// 8scalar (literal init supported)
// 4f32 (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) {

View File

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