diff --git a/Makefile b/Makefile index 6c6d0cc2..b31e02db 100644 --- a/Makefile +++ b/Makefile @@ -339,6 +339,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_arr_u16_store_run \ $(BIN)/test_arr_module_index_run \ $(BIN)/test_arr_float_call_index_run \ + $(BIN)/test_def_float_lit_run \ $(BIN)/test_f64cgen_run \ $(BIN)/test_f64crossmod_run \ $(BIN)/test_tuprecv_run \ @@ -1147,6 +1148,11 @@ $(BIN)/test_arr_float_call_index_run: test/wcc/916_arr_float_call_index_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_def_float_lit_run: test/wcc/917_def_float_lit_run.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_f64cgen_run: test/wcc/951_f64cgen_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 71820c01..3c4f77e9 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -2106,10 +2106,16 @@ 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. */ + if (let_isfloat(n->type)) { + /* Top-level float global (let OR def): same + * LEAQ-indirect shape as str/slice, since + * MOVSS/MOVSD have no D_EXTERN operand form in + * w6a. Pre-#129 this gated on `let_islet` so + * float defs fell through to the MOVQ-AX + * integer-convention fallback below; that + * load-shape mismatched the float storage emit + * (#129 Phase A.1 LOAD-side twin of the + * emit_floatlit_data DATA-side SSoT). */ 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)); @@ -8415,6 +8421,58 @@ emit_data_row_zero(FILE *out, const char *dir, const char *name, int sz) * literal slice syntax to honour, so this is the natural shape. * * Struct lets (size from Type.size): no-init only. */ +/* Emit a (DATA|DATAW) row for a float-typed top-level let/def with a + * FLOATLIT RHS (optionally wrapped in N_CAST or N_UN(±, ...)). Shared + * SSoT for emit_lets's float arm and emit_defs's float arm (#129 + * Phase A.1, rule-12). The N_UN peel mirrors fold_int_literal's + * MINUS/TILDE/PLUS peel (#24) — the float arm had never been given + * the same treatment, so `let g: f64 = -1.5;` silently fell through + * to no-emit + undef-ref at link. Returns 1 on emit, 0 if the rhs + * shape doesn't reduce to a foldable float literal. */ +static int +emit_floatlit_data(FILE *out, Cg *c, const char *directive, + const char *name, Type *t, Node *rhs) +{ + int isf32 = type_isf32(t); + int sz = isf32 ? 4 : 8; + u64 v = 0; + int neg = 0; + if (rhs != NULL) { + Node *r = rhs; + while (r != NULL && r->kind == N_CAST) r = r->lhs; + if (r != NULL && r->kind == N_UN + && (r->op == TK_MINUS || r->op == TK_PLUS)) { + if (r->op == TK_MINUS) neg = 1; + r = r->lhs; + while (r != NULL && r->kind == N_CAST) r = r->lhs; + } + if (r == NULL || r->kind != N_FLOATLIT) return 0; + 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, "%s %s(SB),\"", directive, mod_mangle(c, name)); + /* IEEE-754 sign-bit XOR for negation happens INSIDE the emit + * loop on the top byte only — semantically identical to a whole- + * u64 XOR with 2^63 (or 2^31 for f32) but never materialises + * that constant. Mirrors the wwstage helper's shape so the + * cgen.ww self-rebuild stays cs==ww byte-identical. */ + for (int i = 0; i < sz; i++) { + u8 b = (u8)((v >> (i * 8)) & 0xff); + if (neg && i == sz - 1) + b = (u8)(b ^ 0x80); + emit_data_byte(out, b); + } + fputs("\"\n", out); + return 1; +} + static void emit_lets(Cg *c, FILE *out, Node *file) { @@ -8424,28 +8482,8 @@ emit_lets(Cg *c, FILE *out, Node *file) 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); + (void)emit_floatlit_data(out, c, "DATAW", + d->str, d->type, d->rhs); continue; } if (sz == 8 && !let_isarray(d->type)) { @@ -8578,19 +8616,29 @@ emit_defs(Cg *c, FILE *out, Node *file) for (Node *d = file->list; d; d = d->next) { if (d->kind != N_DEF || d->rhs == NULL) continue; u64 v; - if (!fold_int_literal(d->rhs, &v)) - continue; /* not a fold-able literal constant */ - fprintf(out, "DATA %s(SB),\"", mod_mangle(c, d->str)); - for (int i = 0; i < 8; i++) { - unsigned b = (unsigned)((v >> (i * 8)) & 0xff); - if (b == '"' || b == '\\') - fprintf(out, "\\%c", b); - else if (b < 0x20 || b >= 0x7f) - fprintf(out, "\\x%02x", b); - else - fputc(b, out); + if (fold_int_literal(d->rhs, &v)) { + fprintf(out, "DATA %s(SB),\"", mod_mangle(c, d->str)); + for (int i = 0; i < 8; i++) { + unsigned b = (unsigned)((v >> (i * 8)) & 0xff); + if (b == '"' || b == '\\') + fprintf(out, "\\%c", b); + else if (b < 0x20 || b >= 0x7f) + fprintf(out, "\\x%02x", b); + else + fputc(b, out); + } + fputs("\"\n", out); + continue; + } + /* Float-typed def with FLOATLIT (or N_UN(±,FLOATLIT)) rhs. + * Routes through the same SSoT helper as emit_lets's float + * arm — pre-#129 this fell through to no-emit + undef-ref + * at link. */ + if (let_isfloat(d->type)) { + (void)emit_floatlit_data(out, c, "DATA", + d->str, d->type, d->rhs); + continue; } - fputs("\"\n", out); } (void)c; } diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 4b77885b..999256b3 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -15261,6 +15261,22 @@ fn cgident(c: *cgen, n: *node) void = { return; }; }; + // Float def: load via LEAQ + MOVSS/MOVSD into X0, same shape + // as the let-float arm below — MOVSS/MOVSD have no D_EXTERN + // operand form. Pre-#129 fell through to the MOVQ-AX + // integer-convention fallback, leaving X0 untouched (#129 + // LOAD-side twin of the emitfloatlitdata DATA-side SSoT). + if (isfloattype(c, n)) { + let mov: str = "MOVSD"; + if (isf32type(c, n)) { mov = "MOVSS"; }; + emitline("\tLEAQ\t"); + emitsymname(c, nm); + emitline("(SB), CX\n"); + emitline("\t"); + emitline(mov); + emitline("\t(CX), X0\n"); + return; + }; emitline("\tMOVQ\t"); emitsymname(c, nm); emitline("(SB), AX\n"); @@ -24963,6 +24979,88 @@ export fn letpreintern(c: *cgen, file: *node) void = { // sz struct — zero only. // Non-literal scalar inits and unsupported shapes are skipped so the // link surfaces an undefined-symbol error if the binding is used. +// Emit a (DATA|DATAW) row for a float-typed top-level let/def with a +// FLOATLIT rhs (optionally wrapped in N_CAST or N_UN(±,...)). Shared +// SSoT for emitletdataw float arm + emitdefconstants float arm (#129 +// Phase A.1, rule-12 sea-of-stars). The N_UN(MINUS/PLUS) peel mirrors +// foldintliteral's MINUS/TILDE/PLUS peel (#24); the float arm had +// never been given the same treatment so `let g: f64 = -1.5;` +// silently fell through to no-emit + undef-ref at link. Negation is +// an IEEE-754 sign-bit XOR (bit 63 f64, bit 31 f32) to avoid pulling +// f64/f32 bitcast helpers into cgen. Returns true on emit, false if +// rhs doesn't reduce to a foldable float literal. +fn emitfloatlitdata(c: *cgen, directive: str, name: str, + sz: i32, rhs: *node) bool = { + let isf32: bool = (sz == 4); + let bits: u64 = 0u64; + let neg: bool = false; + if (rhs != nil) { + let r: *node = rhs; + for (r != nil) { + if (r.kind != nkind.N_CAST) { break; }; + r = r.lhs; + }; + if (r != nil) { + if (r.kind == nkind.N_UN) { + if (r.op == tkind.TK_MINUS) { + neg = true; + r = r.lhs; + for (r != nil) { + if (r.kind != nkind.N_CAST) { break; }; + r = r.lhs; + }; + } else { if (r.op == tkind.TK_PLUS) { + r = r.lhs; + for (r != nil) { + if (r.kind != nkind.N_CAST) { break; }; + r = r.lhs; + }; + };}; + }; + }; + if (r == nil) { return false; }; + if (r.kind != nkind.N_FLOATLIT) { return false; }; + // r.uval holds f64 bits regardless of literal suffix (lexer + // stores the pre-narrow bits). f32 needs an explicit + // (double→float) narrowing at emit time — mirrors cstage's + // `union { float f; u32 u; } x; x.f = (float)r->fval` + // (cgen.c:8436). Pre-#129 wwstage truncated the low 4 bytes + // of the f64 bits, which silently emitted 0 for f32 lits; + // the bug never bit because no current consumer has a f32 + // let-init (surfaced by the consolidation gate). + bits = r.uval; + if (isf32) { + let dv: f64 = *((&bits): *f64); + let fv: f32 = (dv: f32); + let uv: u32 = *((&fv): *u32); + bits = uv: u64; + }; + }; + emitline(directive); + emitline(" "); + emitsymname(c, name); + emitline("(SB),\""); + // IEEE-754 sign-bit XOR for negation happens INSIDE the emit + // loop on the top byte only — equivalent to a whole-u64 XOR with + // 2^63 but never materialises that constant. Avoids strconv's + // i64tos-on-i64-MIN bug (#144) and any future cstage const-fold + // of `1 << 63` back to the i64-MIN immediate, either of which + // would break cs==ww byte-id on the cgen.ww self-rebuild (995). + let i: i32 = 0; + let nb: u64 = bits; + for (i < sz) { + let b: u8 = (nb & 255u64): u8; + if (neg) { + if (i == sz - 1) { b = b ^ 128u8; }; + }; + emitdatawbyte(b); + nb = nb >> 8u64; + i += 1; + }; + emitline("\"\n"); + return true; +}; + fn emitletdataw(c: *cgen, file: *node) void = { let d: *node = file.list; for (d != nil) { @@ -24973,42 +25071,14 @@ fn emitletdataw(c: *cgen, file: *node) void = { let issg: bool = letvarisstruct(c, nm); let fsz: i32 = letvarisfloat(c, nm); if (fsz > 0) { - // Float global: 4B (f32) or 8B (f64). - // 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) { - 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((nb & 255u64): u8); - nb = nb >> 8u64; - i += 1; - }; - emitline("\"\n"); - }; + // Float global: routes through the + // emitfloatlitdata SSoT helper, shared + // with emitdefconstants's float arm + // (#129 Phase A.1, rule-12). Bare-call + // discards the bool return (mirrors + // cgen.ww:723 fmt.fprintln pattern). + emitfloatlitdata(c, "DATAW", nm, fsz, + d.rhs); }; // Skip the scalar 8B path when the global is a // fixed-size array that just happens to sum to 8 @@ -25252,6 +25322,27 @@ fn emitdefconstants(c: *cgen, file: *node) void = { if (r != nil) { ok = foldintliteral(r, &v); }; + if (!ok) { + // Float-typed def with FLOATLIT (or N_UN(±,FLOATLIT)) + // rhs: route through the same SSoT helper as + // emitletdataw's float arm. Pre-#129 this fell + // through to no-emit + undef-ref at link. Type-size + // walk mirrors letvarisfloat (#129 Phase A.1). + let dfsz: i32 = 0; + let dt: *node = d.lhs; + for (dt != nil) { + if (dt.kind != nkind.N_TNAME) { dfsz = 0; break; }; + let fsz: i32 = letfloatprim(dt.str); + if (fsz > 0) { dfsz = fsz; break; }; + let nx: *node = aliaslookup(c, dt.str); + if (nx == nil) { dfsz = 0; break; }; + dt = nx; + }; + if (dfsz > 0) { + emitfloatlitdata(c, "DATA", d.str, + dfsz, d.rhs); + }; + }; if (ok) { // #127: route DATA-emit through the SAME emitsymname // SSoT that LOAD/CALL sites use. Replaces the prior diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index 2cfc72f2..d3e06bcf 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -1190,6 +1190,88 @@ export fn letpreintern(c: *cgen, file: *node) void = { // sz struct — zero only. // Non-literal scalar inits and unsupported shapes are skipped so the // link surfaces an undefined-symbol error if the binding is used. +// Emit a (DATA|DATAW) row for a float-typed top-level let/def with a +// FLOATLIT rhs (optionally wrapped in N_CAST or N_UN(±,...)). Shared +// SSoT for emitletdataw float arm + emitdefconstants float arm (#129 +// Phase A.1, rule-12 sea-of-stars). The N_UN(MINUS/PLUS) peel mirrors +// foldintliteral's MINUS/TILDE/PLUS peel (#24); the float arm had +// never been given the same treatment so `let g: f64 = -1.5;` +// silently fell through to no-emit + undef-ref at link. Negation is +// an IEEE-754 sign-bit XOR (bit 63 f64, bit 31 f32) to avoid pulling +// f64/f32 bitcast helpers into cgen. Returns true on emit, false if +// rhs doesn't reduce to a foldable float literal. +fn emitfloatlitdata(c: *cgen, directive: str, name: str, + sz: i32, rhs: *node) bool = { + let isf32: bool = (sz == 4); + let bits: u64 = 0u64; + let neg: bool = false; + if (rhs != nil) { + let r: *node = rhs; + for (r != nil) { + if (r.kind != nkind.N_CAST) { break; }; + r = r.lhs; + }; + if (r != nil) { + if (r.kind == nkind.N_UN) { + if (r.op == tkind.TK_MINUS) { + neg = true; + r = r.lhs; + for (r != nil) { + if (r.kind != nkind.N_CAST) { break; }; + r = r.lhs; + }; + } else { if (r.op == tkind.TK_PLUS) { + r = r.lhs; + for (r != nil) { + if (r.kind != nkind.N_CAST) { break; }; + r = r.lhs; + }; + };}; + }; + }; + if (r == nil) { return false; }; + if (r.kind != nkind.N_FLOATLIT) { return false; }; + // r.uval holds f64 bits regardless of literal suffix (lexer + // stores the pre-narrow bits). f32 needs an explicit + // (double→float) narrowing at emit time — mirrors cstage's + // `union { float f; u32 u; } x; x.f = (float)r->fval` + // (cgen.c:8436). Pre-#129 wwstage truncated the low 4 bytes + // of the f64 bits, which silently emitted 0 for f32 lits; + // the bug never bit because no current consumer has a f32 + // let-init (surfaced by the consolidation gate). + bits = r.uval; + if (isf32) { + let dv: f64 = *((&bits): *f64); + let fv: f32 = (dv: f32); + let uv: u32 = *((&fv): *u32); + bits = uv: u64; + }; + }; + emitline(directive); + emitline(" "); + emitsymname(c, name); + emitline("(SB),\""); + // IEEE-754 sign-bit XOR for negation happens INSIDE the emit + // loop on the top byte only — equivalent to a whole-u64 XOR with + // 2^63 but never materialises that constant. Avoids strconv's + // i64tos-on-i64-MIN bug (#144) and any future cstage const-fold + // of `1 << 63` back to the i64-MIN immediate, either of which + // would break cs==ww byte-id on the cgen.ww self-rebuild (995). + let i: i32 = 0; + let nb: u64 = bits; + for (i < sz) { + let b: u8 = (nb & 255u64): u8; + if (neg) { + if (i == sz - 1) { b = b ^ 128u8; }; + }; + emitdatawbyte(b); + nb = nb >> 8u64; + i += 1; + }; + emitline("\"\n"); + return true; +}; + fn emitletdataw(c: *cgen, file: *node) void = { let d: *node = file.list; for (d != nil) { @@ -1200,42 +1282,14 @@ fn emitletdataw(c: *cgen, file: *node) void = { let issg: bool = letvarisstruct(c, nm); let fsz: i32 = letvarisfloat(c, nm); if (fsz > 0) { - // Float global: 4B (f32) or 8B (f64). - // 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) { - 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((nb & 255u64): u8); - nb = nb >> 8u64; - i += 1; - }; - emitline("\"\n"); - }; + // Float global: routes through the + // emitfloatlitdata SSoT helper, shared + // with emitdefconstants's float arm + // (#129 Phase A.1, rule-12). Bare-call + // discards the bool return (mirrors + // cgen.ww:723 fmt.fprintln pattern). + emitfloatlitdata(c, "DATAW", nm, fsz, + d.rhs); }; // Skip the scalar 8B path when the global is a // fixed-size array that just happens to sum to 8 @@ -1479,6 +1533,27 @@ fn emitdefconstants(c: *cgen, file: *node) void = { if (r != nil) { ok = foldintliteral(r, &v); }; + if (!ok) { + // Float-typed def with FLOATLIT (or N_UN(±,FLOATLIT)) + // rhs: route through the same SSoT helper as + // emitletdataw's float arm. Pre-#129 this fell + // through to no-emit + undef-ref at link. Type-size + // walk mirrors letvarisfloat (#129 Phase A.1). + let dfsz: i32 = 0; + let dt: *node = d.lhs; + for (dt != nil) { + if (dt.kind != nkind.N_TNAME) { dfsz = 0; break; }; + let fsz: i32 = letfloatprim(dt.str); + if (fsz > 0) { dfsz = fsz; break; }; + let nx: *node = aliaslookup(c, dt.str); + if (nx == nil) { dfsz = 0; break; }; + dt = nx; + }; + if (dfsz > 0) { + emitfloatlitdata(c, "DATA", d.str, + dfsz, d.rhs); + }; + }; if (ok) { // #127: route DATA-emit through the SAME emitsymname // SSoT that LOAD/CALL sites use. Replaces the prior diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index aefd69da..e4a20938 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -642,6 +642,22 @@ fn cgident(c: *cgen, n: *node) void = { return; }; }; + // Float def: load via LEAQ + MOVSS/MOVSD into X0, same shape + // as the let-float arm below — MOVSS/MOVSD have no D_EXTERN + // operand form. Pre-#129 fell through to the MOVQ-AX + // integer-convention fallback, leaving X0 untouched (#129 + // LOAD-side twin of the emitfloatlitdata DATA-side SSoT). + if (isfloattype(c, n)) { + let mov: str = "MOVSD"; + if (isf32type(c, n)) { mov = "MOVSS"; }; + emitline("\tLEAQ\t"); + emitsymname(c, nm); + emitline("(SB), CX\n"); + emitline("\t"); + emitline(mov); + emitline("\t(CX), X0\n"); + return; + }; emitline("\tMOVQ\t"); emitsymname(c, nm); emitline("(SB), AX\n"); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 8d32f5e6..54fec17c 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -15261,6 +15261,22 @@ fn cgident(c: *cgen, n: *node) void = { return; }; }; + // Float def: load via LEAQ + MOVSS/MOVSD into X0, same shape + // as the let-float arm below — MOVSS/MOVSD have no D_EXTERN + // operand form. Pre-#129 fell through to the MOVQ-AX + // integer-convention fallback, leaving X0 untouched (#129 + // LOAD-side twin of the emitfloatlitdata DATA-side SSoT). + if (isfloattype(c, n)) { + let mov: str = "MOVSD"; + if (isf32type(c, n)) { mov = "MOVSS"; }; + emitline("\tLEAQ\t"); + emitsymname(c, nm); + emitline("(SB), CX\n"); + emitline("\t"); + emitline(mov); + emitline("\t(CX), X0\n"); + return; + }; emitline("\tMOVQ\t"); emitsymname(c, nm); emitline("(SB), AX\n"); @@ -24963,6 +24979,88 @@ export fn letpreintern(c: *cgen, file: *node) void = { // sz struct — zero only. // Non-literal scalar inits and unsupported shapes are skipped so the // link surfaces an undefined-symbol error if the binding is used. +// Emit a (DATA|DATAW) row for a float-typed top-level let/def with a +// FLOATLIT rhs (optionally wrapped in N_CAST or N_UN(±,...)). Shared +// SSoT for emitletdataw float arm + emitdefconstants float arm (#129 +// Phase A.1, rule-12 sea-of-stars). The N_UN(MINUS/PLUS) peel mirrors +// foldintliteral's MINUS/TILDE/PLUS peel (#24); the float arm had +// never been given the same treatment so `let g: f64 = -1.5;` +// silently fell through to no-emit + undef-ref at link. Negation is +// an IEEE-754 sign-bit XOR (bit 63 f64, bit 31 f32) to avoid pulling +// f64/f32 bitcast helpers into cgen. Returns true on emit, false if +// rhs doesn't reduce to a foldable float literal. +fn emitfloatlitdata(c: *cgen, directive: str, name: str, + sz: i32, rhs: *node) bool = { + let isf32: bool = (sz == 4); + let bits: u64 = 0u64; + let neg: bool = false; + if (rhs != nil) { + let r: *node = rhs; + for (r != nil) { + if (r.kind != nkind.N_CAST) { break; }; + r = r.lhs; + }; + if (r != nil) { + if (r.kind == nkind.N_UN) { + if (r.op == tkind.TK_MINUS) { + neg = true; + r = r.lhs; + for (r != nil) { + if (r.kind != nkind.N_CAST) { break; }; + r = r.lhs; + }; + } else { if (r.op == tkind.TK_PLUS) { + r = r.lhs; + for (r != nil) { + if (r.kind != nkind.N_CAST) { break; }; + r = r.lhs; + }; + };}; + }; + }; + if (r == nil) { return false; }; + if (r.kind != nkind.N_FLOATLIT) { return false; }; + // r.uval holds f64 bits regardless of literal suffix (lexer + // stores the pre-narrow bits). f32 needs an explicit + // (double→float) narrowing at emit time — mirrors cstage's + // `union { float f; u32 u; } x; x.f = (float)r->fval` + // (cgen.c:8436). Pre-#129 wwstage truncated the low 4 bytes + // of the f64 bits, which silently emitted 0 for f32 lits; + // the bug never bit because no current consumer has a f32 + // let-init (surfaced by the consolidation gate). + bits = r.uval; + if (isf32) { + let dv: f64 = *((&bits): *f64); + let fv: f32 = (dv: f32); + let uv: u32 = *((&fv): *u32); + bits = uv: u64; + }; + }; + emitline(directive); + emitline(" "); + emitsymname(c, name); + emitline("(SB),\""); + // IEEE-754 sign-bit XOR for negation happens INSIDE the emit + // loop on the top byte only — equivalent to a whole-u64 XOR with + // 2^63 but never materialises that constant. Avoids strconv's + // i64tos-on-i64-MIN bug (#144) and any future cstage const-fold + // of `1 << 63` back to the i64-MIN immediate, either of which + // would break cs==ww byte-id on the cgen.ww self-rebuild (995). + let i: i32 = 0; + let nb: u64 = bits; + for (i < sz) { + let b: u8 = (nb & 255u64): u8; + if (neg) { + if (i == sz - 1) { b = b ^ 128u8; }; + }; + emitdatawbyte(b); + nb = nb >> 8u64; + i += 1; + }; + emitline("\"\n"); + return true; +}; + fn emitletdataw(c: *cgen, file: *node) void = { let d: *node = file.list; for (d != nil) { @@ -24973,42 +25071,14 @@ fn emitletdataw(c: *cgen, file: *node) void = { let issg: bool = letvarisstruct(c, nm); let fsz: i32 = letvarisfloat(c, nm); if (fsz > 0) { - // Float global: 4B (f32) or 8B (f64). - // 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) { - 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((nb & 255u64): u8); - nb = nb >> 8u64; - i += 1; - }; - emitline("\"\n"); - }; + // Float global: routes through the + // emitfloatlitdata SSoT helper, shared + // with emitdefconstants's float arm + // (#129 Phase A.1, rule-12). Bare-call + // discards the bool return (mirrors + // cgen.ww:723 fmt.fprintln pattern). + emitfloatlitdata(c, "DATAW", nm, fsz, + d.rhs); }; // Skip the scalar 8B path when the global is a // fixed-size array that just happens to sum to 8 @@ -25252,6 +25322,27 @@ fn emitdefconstants(c: *cgen, file: *node) void = { if (r != nil) { ok = foldintliteral(r, &v); }; + if (!ok) { + // Float-typed def with FLOATLIT (or N_UN(±,FLOATLIT)) + // rhs: route through the same SSoT helper as + // emitletdataw's float arm. Pre-#129 this fell + // through to no-emit + undef-ref at link. Type-size + // walk mirrors letvarisfloat (#129 Phase A.1). + let dfsz: i32 = 0; + let dt: *node = d.lhs; + for (dt != nil) { + if (dt.kind != nkind.N_TNAME) { dfsz = 0; break; }; + let fsz: i32 = letfloatprim(dt.str); + if (fsz > 0) { dfsz = fsz; break; }; + let nx: *node = aliaslookup(c, dt.str); + if (nx == nil) { dfsz = 0; break; }; + dt = nx; + }; + if (dfsz > 0) { + emitfloatlitdata(c, "DATA", d.str, + dfsz, d.rhs); + }; + }; if (ok) { // #127: route DATA-emit through the SAME emitsymname // SSoT that LOAD/CALL sites use. Replaces the prior diff --git a/test/wcc/917_def_float_lit_run.c b/test/wcc/917_def_float_lit_run.c new file mode 100644 index 00000000..f0bc55f5 --- /dev/null +++ b/test/wcc/917_def_float_lit_run.c @@ -0,0 +1,235 @@ +/* + * 917_def_float_lit_run — runtime + byte-id net for #129 Phase A.1: + * module-level `def NAME: f64 = literal;` (and `def: f32 = …`) + * silently fell through emit_defs's int-only `fold_int_literal` gate, + * so no DATAW row landed in the .data section — `w6l` failed link + * with `undefined reference to 'main.NAME'`. Symmetric latent bug in + * emit_lets's float arm: it only handled bare N_FLOATLIT, never + * peeled N_UN(MINUS/PLUS, N_FLOATLIT), so `let g: f64 = -1.5;` + * silently zero-emitted into the same undef-ref shape. + * + * Phase A.1 fix (rule-12 sea-of-stars consolidation): + * - cstage: extract `emit_floatlit_data(out, c, dir, name, T, rhs)` + * helper, called by emit_lets's float arm (now collapsed) AND + * emit_defs (new float arm). N_CAST + N_UN(MINUS/PLUS, + * N_FLOATLIT) peeled inside the helper. + * - wwstage: parallel `emitfloatlitdata` helper. Negation via + * IEEE-754 sign-bit XOR (avoids dragging the math bitcast helpers + * into cgen). + * - LOAD side: cstage's N_IDENT float-let arm and wwstage's cgident + * def-branch both gated on let_islet/deflookup-but-not-float; the + * gate widens to also catch float defs (they emit through the + * same mod_mangle / emitsymname symbol). + * + * Bootstrap NEUTRAL: zero current `def: f{32,64} = lit` consumers in + * lib/ or selfhost/. The N_UN-peel collateral fix for lets has no + * shipped consumers either (audit: no `let g: f64 = -lit;` anywhere). + * + * Rows cover both fix sides (DATA emit + LOAD) and the N_UN collateral: + * - f64_def_pos: `def K: f64 = 1.5;` — was undef-ref pre-fix, exit + * 1 (1.5 → i32 truncates to 1). + * - f64_def_neg: `def K: f64 = -1.5;` — was undef-ref pre-fix; exit + * 255 (sign-bit flip via XOR; -1.5 → i32 truncates to -1 → 255 + * in unsigned exit byte). + * - f32_def_pos: `def K: f32 = 1.5f32; …` — same shape, f32 width. + * - f64_let_neg: `let g: f64 = -1.5;` — pre-existing latent bug + * (N_UN peel never added to emit_lets); now fixed by the same + * helper. + * - f64_let_pos: `let g: f64 = 1.5;` — regression guard (existing + * bare-N_FLOATLIT path). + * - f32_let_pos: `let g: f32 = 1.5f32;` — regression guard, f32 path. + * + * Each row carries (a) cstage `ww build` + run asserting exit code + * and (b) w6c vs w6c_ww `.s` cmp (rule-10 byte-id). + */ +#include +#include +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return -1; +} + +struct row { const char *label; const char *src; int want_exit; }; + +static const struct row rows[] = { + { "f64_def_pos", + "package main;\n" + "def K: f64 = 1.5;\n" + "export fn main() i32 = {\n" + " let x: f64 = K;\n" + " return (x: i32);\n" + "};\n", 1 }, + { "f64_def_neg", + "package main;\n" + "def K: f64 = -1.5;\n" + "export fn main() i32 = {\n" + " let x: f64 = K;\n" + " return (x: i32);\n" + "};\n", 255 /* -1 as unsigned exit byte */ }, + { "f32_def_pos", + "package main;\n" + "def K: f32 = 1.5f32;\n" + "export fn main() i32 = {\n" + " let x: f32 = K;\n" + " return (x: i32);\n" + "};\n", 1 }, + /* Matrix-closure: f32 + neg combined directly. Exercises the + * f32-narrow path PLUS the top-byte-XOR negation in one row. + * The other rows cover each leg individually (f64_def_neg for + * negation, f32_def_pos for f32 narrow); this row pins they + * compose correctly. */ + { "f32_def_neg", + "package main;\n" + "def K: f32 = -1.5f32;\n" + "export fn main() i32 = {\n" + " let x: f32 = K;\n" + " return (x: i32);\n" + "};\n", 255 /* -1 as exit byte */ }, + /* Pre-existing latent: emit_lets's float arm never peeled + * N_UN(MINUS,N_FLOATLIT). The class-closure consolidation + * fix in emit_floatlit_data heals this too. */ + { "f64_let_neg", + "package main;\n" + "let g: f64 = -1.5;\n" + "export fn main() i32 = {\n" + " return (g: i32);\n" + "};\n", 255 }, + { "f64_let_pos", + "package main;\n" + "let g: f64 = 1.5;\n" + "export fn main() i32 = {\n" + " return (g: i32);\n" + "};\n", 1 }, + { "f32_let_pos", + "package main;\n" + "let g: f32 = 1.5f32;\n" + "export fn main() i32 = {\n" + " return (g: i32);\n" + "};\n", 1 }, + { NULL, NULL, 0 } +}; + +static int +slurp_eq(const char *a, const char *b) +{ + FILE *fa = fopen(a, "rb"); + FILE *fb = fopen(b, "rb"); + if (!fa || !fb) { if (fa) fclose(fa); if (fb) fclose(fb); return -1; } + int rc = 0; + for (;;) { + int ca = fgetc(fa); + int cb = fgetc(fb); + if (ca != cb) { rc = -1; break; } + if (ca == EOF) break; + } + fclose(fa); fclose(fb); + return rc; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char w6c[1100], w6c_ww[1100]; + snprintf(w6c, sizeof w6c, "%s/w6c", bin); + snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); + if (access(w6c_ww, X_OK) != 0) { + fprintf(stderr, "deflitf: w6c_ww missing — cannot run " + "the cs==ww byte-id gate (the whole point of this test)\n"); + return 1; + } + + int n = 0, fail = 0; + for (int i = 0; rows[i].src; i++, n++) { + char src[64]; + snprintf(src, sizeof src, "/tmp/wwdflf_%d_%d.ww", getpid(), i); + FILE *f = fopen(src, "wb"); + if (f == NULL) { fail++; continue; } + fputs(rows[i].src, f); + fclose(f); + + char tmpdir[64]; + snprintf(tmpdir, sizeof tmpdir, "/tmp/wwdflf_%d_d_%d", + getpid(), i); + mkdir(tmpdir, 0755); + + char cmd[2048]; + snprintf(cmd, sizeof cmd, "cd %s && %s/ww build %s", + tmpdir, bin, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: cstage build failed\n", + rows[i].label); + fail++; + unlink(src); rmdir(tmpdir); + continue; + } + + char outbin[128]; + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + + int got = runwait(outbin); + if (got != rows[i].want_exit) { + fprintf(stderr, "row[%s]: cstage exit %d, want %d\n", + rows[i].label, got, rows[i].want_exit); + fail++; + } + unlink(outbin); rmdir(tmpdir); + + char cs_s[64], ws_s[64]; + snprintf(cs_s, sizeof cs_s, "/tmp/wwdflf_%d_%d_cs.s", + getpid(), i); + snprintf(ws_s, sizeof ws_s, "/tmp/wwdflf_%d_%d_ww.s", + getpid(), i); + + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", + w6c, cs_s, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c failed\n", rows[i].label); + fail++; unlink(src); continue; + } + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", + w6c_ww, ws_s, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c_ww failed\n", + rows[i].label); + fail++; unlink(src); unlink(cs_s); continue; + } + if (slurp_eq(cs_s, ws_s) != 0) { + fprintf(stderr, + "row[%s]: cstage/wwstage .s DIFFER (rule-10 " + "byte-id violation)\n", rows[i].label); + fail++; + } + unlink(src); unlink(cs_s); unlink(ws_s); + } + + if (fail) { + fprintf(stderr, "%d/%d def-float-lit tests failed\n", fail, n); + return 1; + } + printf("deflitf: %d/%d ok (cstage run + cs==ww byte-id)\n", + n, n); + return 0; +}