diff --git a/Makefile b/Makefile index d14f6deb..87e6d1ad 100644 --- a/Makefile +++ b/Makefile @@ -238,6 +238,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_redecl \ $(BIN)/test_struct_field_index \ $(BIN)/test_tagged_return_scratch \ + $(BIN)/test_tagged_widen_f64 \ $(BIN)/test_param_shadow_mod \ $(BIN)/test_localoff_scope \ $(BIN)/test_cast_enum_movl \ @@ -477,6 +478,12 @@ $(BIN)/test_tagged_return_scratch: test/wcc/714_tagged_return_scratch.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_tagged_widen_f64: test/wcc/715_tagged_widen_f64.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_use_promote_alias: test/wcc/699_use_promote_alias.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 4d2d9284..c29ce03f 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -1295,6 +1295,25 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src, if (via_outer) goto copy_out; return; } + /* Float arm: cgexpr on an f64/f32 source leaves the bit pattern in + * X0 only — the AX-store below would silently write whatever was + * loaded into AX before the SSE conversion. Literal `1.0` works by + * coincidence (TK_FLOAT lowering loads the f64 bit pattern into AX + * before MOVSD'ing into X0); every runtime f64 shape (cast, call, + * unary, ident, struct-field load) needs the explicit MOVSD path. + * Same kind-specific dispatch as the str/slice branches above and + * the structlit field-flow at the top of this function. */ + int wid_isf32 = 0; + if (fld_isfloat(st, &wid_isf32)) { + int mov = wid_isf32 ? A_MOVSS : A_MOVSD; + cgexpr(c, src, *locals_p); + ins2(c, mov, areg(D_X0), amem(D_BP, write_off + 8)); + int tag = cg_tag_for_variant(du, st); + ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag), + amem(D_BP, write_off + 0)); + if (via_outer) goto copy_out; + return; + } /* Scalar / pointer / etc. The high slot word (when sz > 16) is * left untouched here — match dispatches on the tag word first * and only the str branch reads slot+16, so leaving the pad diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 4acc0c13..1d9259aa 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -9110,6 +9110,37 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *node, src: *node, slot_off: i32, slot_sz emitline("(BP)\n"); return; }; + // Float arm: cgexpr on an f64/f32 source leaves the bit pattern in + // X0 only — the AX-store fallback below would silently write whatever + // was loaded into AX before the SSE conversion. Literal `1.0` works + // by coincidence (TK_FLOAT lowering loads the f64 bit pattern into AX + // before MOVSD'ing into X0); every runtime f64 shape (cast, call, + // unary, ident, struct-field load) needs the explicit MOVSD path. + // Mirror of cstage cg_widen_tagged_store's float arm. Wwstage has no + // checker so we classify via exprfloatkind (same shape used by cgcast) + // and resolve the variant tag by name directly — rhstargetname has no + // N_FLOATLIT / N_CALL / N_DOT branch and would fall through to the + // str-shape fallback that picks tag 0 for an `(i64 | f64)` union. + let fkind: i32 = exprfloatkind(c, src); + if (fkind != 0) { + let fmov: str = "MOVSD"; + let fname: str = "f64"; + if (fkind == 1) { fmov = "MOVSS"; fname = "f32"; }; + cgexpr(c, src); + emitline("\t"); + emitline(fmov); + emitline("\tX0, "); + emitoff((slot_off + 8): i64); + emitline("(BP)\n"); + let ftag: i32 = flatvariantidx(c, dt, fname); + if (ftag < 0) { ftag = 0; }; + emitline("\tMOVQ\t$"); + emitint(ftag: i64); + emitline(", "); + emitoff(slot_off: i64); + emitline("(BP)\n"); + return; + }; // Scalar payload. cgexpr(c, src); emitline("\tMOVQ\tAX, "); diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index f6a0e213..5d509640 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -2867,6 +2867,37 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *node, src: *node, slot_off: i32, slot_sz emitline("(BP)\n"); return; }; + // Float arm: cgexpr on an f64/f32 source leaves the bit pattern in + // X0 only — the AX-store fallback below would silently write whatever + // was loaded into AX before the SSE conversion. Literal `1.0` works + // by coincidence (TK_FLOAT lowering loads the f64 bit pattern into AX + // before MOVSD'ing into X0); every runtime f64 shape (cast, call, + // unary, ident, struct-field load) needs the explicit MOVSD path. + // Mirror of cstage cg_widen_tagged_store's float arm. Wwstage has no + // checker so we classify via exprfloatkind (same shape used by cgcast) + // and resolve the variant tag by name directly — rhstargetname has no + // N_FLOATLIT / N_CALL / N_DOT branch and would fall through to the + // str-shape fallback that picks tag 0 for an `(i64 | f64)` union. + let fkind: i32 = exprfloatkind(c, src); + if (fkind != 0) { + let fmov: str = "MOVSD"; + let fname: str = "f64"; + if (fkind == 1) { fmov = "MOVSS"; fname = "f32"; }; + cgexpr(c, src); + emitline("\t"); + emitline(fmov); + emitline("\tX0, "); + emitoff((slot_off + 8): i64); + emitline("(BP)\n"); + let ftag: i32 = flatvariantidx(c, dt, fname); + if (ftag < 0) { ftag = 0; }; + emitline("\tMOVQ\t$"); + emitint(ftag: i64); + emitline(", "); + emitoff(slot_off: i64); + emitline("(BP)\n"); + return; + }; // Scalar payload. cgexpr(c, src); emitline("\tMOVQ\tAX, "); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 85d9c22a..7aa24514 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -9110,6 +9110,37 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *node, src: *node, slot_off: i32, slot_sz emitline("(BP)\n"); return; }; + // Float arm: cgexpr on an f64/f32 source leaves the bit pattern in + // X0 only — the AX-store fallback below would silently write whatever + // was loaded into AX before the SSE conversion. Literal `1.0` works + // by coincidence (TK_FLOAT lowering loads the f64 bit pattern into AX + // before MOVSD'ing into X0); every runtime f64 shape (cast, call, + // unary, ident, struct-field load) needs the explicit MOVSD path. + // Mirror of cstage cg_widen_tagged_store's float arm. Wwstage has no + // checker so we classify via exprfloatkind (same shape used by cgcast) + // and resolve the variant tag by name directly — rhstargetname has no + // N_FLOATLIT / N_CALL / N_DOT branch and would fall through to the + // str-shape fallback that picks tag 0 for an `(i64 | f64)` union. + let fkind: i32 = exprfloatkind(c, src); + if (fkind != 0) { + let fmov: str = "MOVSD"; + let fname: str = "f64"; + if (fkind == 1) { fmov = "MOVSS"; fname = "f32"; }; + cgexpr(c, src); + emitline("\t"); + emitline(fmov); + emitline("\tX0, "); + emitoff((slot_off + 8): i64); + emitline("(BP)\n"); + let ftag: i32 = flatvariantidx(c, dt, fname); + if (ftag < 0) { ftag = 0; }; + emitline("\tMOVQ\t$"); + emitint(ftag: i64); + emitline(", "); + emitoff(slot_off: i64); + emitline("(BP)\n"); + return; + }; // Scalar payload. cgexpr(c, src); emitline("\tMOVQ\tAX, "); diff --git a/test/wcc/715_tagged_widen_f64.c b/test/wcc/715_tagged_widen_f64.c new file mode 100644 index 00000000..c4c3471d --- /dev/null +++ b/test/wcc/715_tagged_widen_f64.c @@ -0,0 +1,237 @@ +/* + * 715_tagged_widen_f64 — variant-widen into a tagged-union slot must + * route f64/f32 sources through the SSE register (X0) instead of the + * integer AX. Pre-fix cg_widen_tagged_store / cgwidentaggedstorebp fell + * through to a single "scalar payload" arm that always emitted + * `MOVQ AX, slot+8(BP)`; for an f64 source cgexpr leaves the bit pattern + * in X0 only and AX holds the pre-conversion integer (or any prior + * temp). The literal-f64 case worked by coincidence because TK_FLOAT + * lowering happens to load the f64 bit pattern into AX before the + * MOVSD into X0; every runtime-f64 shape (cast, call, unary, ident, + * struct-field load) silently miscompiled. + * + * Surfaced by worker-fmtfloat probe during the #17 (fmt float dispatch + * arm) pre-flight. Closed by adding an `fld_isfloat(st, ...)` / + * `exprfloatkind(c, src)` arm ahead of the scalar fallback in both + * stages — same kind-specific dispatch as the existing str / slice + * branches and the structlit-field-flow MOVSD arm. + * + * Coverage is bit-level: each row punts the tagged-union slot through + * `*u8` and reads payload bits as u64, so a tag-only or low-32-bits-only + * store fails the row instead of silently approximating. The expected + * payload for f64 1.0 is 0x3FF0000000000000 == 4607182418800017408. + * + * Rows: + * literal_1_0 — `1.0` typed by surface form. Pre-fix passed + * by coincidence (MOVQ AX path happened to + * hold the right bits); pin it explicitly so + * a future cgexpr refactor that changes the + * constant-load shape can't silently regress. + * cast_1_f64 — `1: f64`. Cast-peel keeps the cast (dest is + * a concrete variant, not the union), so the + * CVTSI2SD's X0 result must flow through + * MOVSD. + * call_makeone — fn returning f64. X0-ABI return. + * unary_neg_f64 — `-(1: f64)`. N_UN(MINUS, N_CAST) over f64; + * cgun keeps the value in X0. + * ident_f64 — concrete f64 local. cgexpr emits a MOVSD + * load to X0; AX is untouched. + * field_f64 — load from a struct field of type f64. + * Exercises the field-read shape through + * cgexpr; same X0-only ABI. + * + * One extra row pins the negative case: an i64 rhs of the same union + * must still emit MOVQ AX (the float arm is gated on the source's + * float-class, not on the dst variant set). + * + * Note: this is a regression cover for the same path that blocks #17 + * (fmt float dispatch arm). No in-tree caller exercised the path + * pre-fix, hence the latent silence; #17 will be the first consumer. + */ +#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; }; + +/* Bit-pin via `*u8` cast → payload at slot+8, tag at slot+0. Returns + * 0 if payload bits and tag both match expected + * 1 if tag wrong + * 2 if payload bits wrong (the f64 miscompile signature) + * Driver checks `got == 0`. */ +#define BITCHECK_F64(union_decl, init_stmt, want_bits, want_tag) \ + union_decl \ + "fn main() i32 = {\n" \ + " " init_stmt \ + " let pp: *(i64 | f64) = &a;\n" \ + " let pu: *u8 = pp: *u8;\n" \ + " let tagp: *i64 = pu: *i64;\n" \ + " let payp: *u64 = (pu + 8u64): *u64;\n" \ + " if (*tagp != " want_tag "i64) { return 1; };\n" \ + " if (*payp != " want_bits "u64) { return 2; };\n" \ + " return 0;\n" \ + "};\n" + +static const struct row rows[] = { + /* Literal 1.0 — already-correct lock-in. TK_FLOAT lowering + * loaded the f64 bits into AX so MOVQ wrote the right value; + * after the fix it goes through the principled MOVSD path. */ + { "literal_1_0", + BITCHECK_F64( + "", + "let a: (i64 | f64) = 1.0;\n", + "4607182418800017408", "1"), + 0 }, + /* Runtime cast: `1: f64`. Pre-fix wrote integer 1 into payload. */ + { "cast_1_f64", + BITCHECK_F64( + "", + "let a: (i64 | f64) = 1: f64;\n", + "4607182418800017408", "1"), + 0 }, + /* Fn returning f64 — X0-ABI return. cgexpr on N_CALL leaves the + * value in X0, AX holds the return-value index / 0. */ + { "call_makeone", + BITCHECK_F64( + "fn makeone() f64 = { return 1: f64; };\n", + "let a: (i64 | f64) = makeone();\n", + "4607182418800017408", "1"), + 0 }, + /* Unary minus over an f64 cast — value stays in X0 through cgun. */ + { "unary_neg_f64", + BITCHECK_F64( + "", + "let a: (i64 | f64) = -(1: f64);\n", + /* -1.0 == 0xBFF0000000000000 == 13830554455654793216 */ + "13830554455654793216", "1"), + 0 }, + /* Concrete f64 ident — cgexpr emits MOVSD load to X0; AX is + * never touched. */ + { "ident_f64", + BITCHECK_F64( + "", + "let f: f64 = 1: f64;\n" + " let a: (i64 | f64) = f;\n", + "4607182418800017408", "1"), + 0 }, + /* Struct-field of type f64 — exercises the field-read shape + * through cgexpr. Same X0-only ABI as plain idents. */ + { "field_f64", + BITCHECK_F64( + "type holder = struct { v: f64, pad: i32 };\n", + "let h: holder;\n" + " h.v = 1: f64;\n" + " let a: (i64 | f64) = h.v;\n", + "4607182418800017408", "1"), + 0 }, + /* Negative control: i64 rhs into the same union must still hit + * the integer arm (MOVQ AX). Pre-fix this also worked; the float + * arm must not shadow it. Tag = 0 (i64 is variant 0). */ + { "i64_rhs_still_integer", + BITCHECK_F64( + "", + "let a: (i64 | f64) = 7i64;\n", + "7", "0"), + 0 }, +}; + +static int +run_driver(const char *driver, const struct row *r, int i) +{ + char src[64], tmpdir[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/twf64_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/twf64_%d_d_%d", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %s build %s", + tmpdir, driver, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: build via %s failed\n", + r->label, driver); + unlink(src); rmdir(tmpdir); + return -1; + } + + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + char outbin[128]; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + int got = runwait(outbin); + + unlink(src); unlink(outbin); rmdir(tmpdir); + return got; +} + +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 cdrv[1024]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + char wdrv[1024]; + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + struct { const char *name; const char *path; int gated_on_existence; } + drivers[] = { + { "cstage", cdrv, 0 }, + { "wwstage", wdrv, 1 }, + { NULL, NULL, 0 }, + }; + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + for (int d = 0; drivers[d].name; d++) { + if (drivers[d].gated_on_existence + && access(drivers[d].path, X_OK) != 0) { + fprintf(stderr, "tagged_widen_f64: skip %s (no %s)\n", + drivers[d].name, drivers[d].path); + continue; + } + for (int i = 0; i < n; i++) { + int got = run_driver(drivers[d].path, &rows[i], i); + total++; + if (got != rows[i].want) { + fprintf(stderr, + "tagged_widen_f64[%s][%s]: exit=%d want=%d\n", + drivers[d].name, rows[i].label, + got, rows[i].want); + fail++; + } + } + } + if (fail) { + fprintf(stderr, + "tagged_widen_f64: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("tagged_widen_f64: %d/%d ok\n", total, total); + return 0; +}