diff --git a/Makefile b/Makefile index 9bee4422..0299bf9e 100644 --- a/Makefile +++ b/Makefile @@ -402,6 +402,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_fmt_handle_run \ $(BIN)/test_fmt_mods_run \ $(BIN)/test_fmt_compositions_run \ + $(BIN)/test_fmt_int_run \ $(BIN)/test_fieldfn_leaf_collide_run \ $(BIN)/test_amp_fn_assign_run \ $(BIN)/test_type_value_shadow_run \ @@ -1245,6 +1246,14 @@ $(BIN)/test_fmt_compositions_run: test/wcc/781_fmt_compositions_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_fmt_int_run: test/wcc/815_fmt_int_run.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + lib/fmt/fmt.ww \ + lib/memio/memio.ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_bufio_vstream_run: test/wcc/778_bufio_vstream_run.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/lib/fmt/fmt.ww b/lib/fmt/fmt.ww index 80e269ec..17329971 100644 --- a/lib/fmt/fmt.ww +++ b/lib/fmt/fmt.ww @@ -73,7 +73,18 @@ fn i64dec(v: i64) str = { // caller. Callers with an `f32` cast at the call site (`myf: f64`), // mirroring how `i64` covers every int width today. Ship the `f32` // arm when the first in-tree caller needs it. -export type formattable = (i64 | str | bool | rune | f64); +// +// `int`/`uint` appended LAST: variant tags follow declaration order +// (cstage cg_tag_for_variant / wwstage flatvariantidxt), so the +// pre-existing tags i64=0,str=1,bool=2,rune=3,f64=4 stay frozen and +// int=5,uint=6 — zero byte-id churn for existing callers (#6). This is a +// STAGED step toward Hare's full `types::numeric` (ref/hare/types/ +// classes.ha:5-17 → ref/hare/fmt/iter.ha:14): int/uint are the machine- +// word types apps actually print; narrower widths (i8/i16/i32, u8/u16/ +// u32, size) graduate when a caller lands. Making them real members (not +// a size/name-keyed coercion) closes the #128 int-path leniency by +// construction — both stages now accept bare int by MEMBERSHIP. +export type formattable = (i64 | str | bool | rune | f64 | int | uint); // ---- internal stream formatters -------------------------------------- @@ -111,6 +122,15 @@ fn writeone(s: io.handle, a: formattable) (size | io.error) = { let v2: str = strconv.f64tos(v); return putbytes(s, v2.ptr, v2.len); }; + case let n: int => { + let v: str = i64dec(n: i64); + return putbytes(s, v.ptr, v.len); + }; + case let n: uint => { + // unsigned path: i64dec would render a high-bit value negative. + let v: str = strconv.u64tos(n: u64, strconv.base.DEC); + return putbytes(s, v.ptr, v.len); + }; }; return 0: size; }; @@ -281,6 +301,19 @@ fn rawleni64(v: i64, m: *mods) i32 = { return signlen + inner; }; +// rawlenu64 — bytes the raw render of unsigned `v` under `m` would +// emit. uint twin of [[rawleni64]]: no value-derived sign (uint is +// never negative), so neg_flag is fixed false — only an explicit +// PLUS/SPACE mod adds a sign byte. Mirror print.ha. +fn rawlenu64(v: u64, m: *mods) i32 = { + let signlen: i32 = 0; + if (signof(false, m) != 0u8) { signlen = 1; }; + let dlen: i32 = digitsu64(v, basenum(m.base)); + let inner: i32 = dlen; + if (m.prec > signlen + dlen) { inner = m.prec - signlen; }; + return signlen + inner; +}; + // rawlenstr — bytes the raw render of `s` would emit (after `prec` // truncation, per Hare print.ha:86). fn rawlenstr(s: str, m: *mods) i32 = { @@ -314,6 +347,8 @@ fn rawlen(arg: formattable, m: *mods) i32 = { case let b: bool => { if (b) { return 4; }; return 5; }; case let r: rune => return 1; case let v: f64 => return rawlenf64(v, m); + case let v: int => return rawleni64(v: i64, m); + case let v: uint => return rawlenu64(v: u64, m); }; return 0; // unreachable — match is exhaustive }; @@ -408,6 +443,88 @@ fn formatraw(s: io.handle, arg: formattable, m: *mods) (size | io.error) = { }; return total; }; + case let vi: int => { + // int is the signed machine word; widen to i64 and emit the + // signed render — identical to the i64 arm above. + let v: i64 = vi: i64; + let neg_flag: bool = v < 0; + let u: u64 = v: u64; + if (neg_flag) { u = (-v): u64; }; + let sb: u8 = signof(neg_flag, m); + let total: size = 0; + if (sb != 0u8) { + let buf: [1]u8; + buf[0] = sb; + let r: (size | io.error) = putbytes(s, &buf[0], 1); + match (r) { + case let n: size => { total += n; }; + case let e: io.error => return e; + }; + }; + let dlen: i32 = digitsu64(u, basenum(m.base)); + let signlen: i32 = 0; + if (sb != 0u8) { signlen = 1; }; + let pad0: i32 = 0; + if (m.prec > signlen + dlen) { pad0 = m.prec - signlen - dlen; }; + let pi: i32 = 0; + for (pi < pad0) { + let buf: [1]u8; + buf[0] = '0'; + let r: (size | io.error) = putbytes(s, &buf[0], 1); + match (r) { + case let n: size => { total += n; }; + case let e: io.error => return e; + }; + pi += 1; + }; + let view: str = strconv.u64tos(u, m.base); + let r: (size | io.error) = putbytes(s, view.ptr, view.len); + match (r) { + case let n: size => { total += n; }; + case let e: io.error => return e; + }; + return total; + }; + case let vu: uint => { + // unsigned path: no value-derived sign (uint never negative), so + // neg_flag is fixed false — a high-bit value renders as its true + // unsigned decimal, not negative. strconv.u64tos, not i64dec. + let u: u64 = vu: u64; + let sb: u8 = signof(false, m); + let total: size = 0; + if (sb != 0u8) { + let buf: [1]u8; + buf[0] = sb; + let r: (size | io.error) = putbytes(s, &buf[0], 1); + match (r) { + case let n: size => { total += n; }; + case let e: io.error => return e; + }; + }; + let dlen: i32 = digitsu64(u, basenum(m.base)); + let signlen: i32 = 0; + if (sb != 0u8) { signlen = 1; }; + let pad0: i32 = 0; + if (m.prec > signlen + dlen) { pad0 = m.prec - signlen - dlen; }; + let pi: i32 = 0; + for (pi < pad0) { + let buf: [1]u8; + buf[0] = '0'; + let r: (size | io.error) = putbytes(s, &buf[0], 1); + match (r) { + case let n: size => { total += n; }; + case let e: io.error => return e; + }; + pi += 1; + }; + let view: str = strconv.u64tos(u, m.base); + let r: (size | io.error) = putbytes(s, view.ptr, view.len); + match (r) { + case let n: size => { total += n; }; + case let e: io.error => return e; + }; + return total; + }; }; let z: size = 0; return z; // unreachable — match is exhaustive }; @@ -485,6 +602,14 @@ fn formatfield(s: io.handle, f: field, m: *mods) (size | io.error) = { let a: formattable = v; return formatone(s, a, m); }; + case let v: int => { + let a: formattable = v; + return formatone(s, a, m); + }; + case let v: uint => { + let a: formattable = v; + return formatone(s, a, m); + }; case let p: *mods => { fmtabort(); let z: size = 0; return z; }; }; }; diff --git a/test/wcc/815_fmt_int_run.c b/test/wcc/815_fmt_int_run.c new file mode 100644 index 00000000..dcba77be --- /dev/null +++ b/test/wcc/815_fmt_int_run.c @@ -0,0 +1,398 @@ +/* + * 815_fmt_int_run — #6 fmt.formattable widen (bare int / uint). Pins + * that lib/fmt/fmt.ww now renders bare `int` and `uint` as REAL union + * members (formattable = (i64|str|bool|rune|f64|int|uint), int=5, + * uint=6 appended last). Before #6 cstage LOUD-rejected a bare int arg + * to the fmt family (no `int` arm to widen into) while wwstage leniently + * accepted via the #128 size/name-keyed int->i64 coercion — cs!=ww. + * Making int/uint real members makes BOTH accept by membership; this + * test is the cstage-now-accepts + correct-render + cs==ww byte-id gate. + * + * row | what it pins + * ---------------+-------------------------------------------------- + * bare_int | fprint(int 42) over the writeone path -> "42" + * bare_int_neg | fprint(int -7) -> "-7" (signed i64dec path) + * bare_uint | fprint(uint 42) -> "42" + * uint_highbit | fprint(uint 2^63+1) -> "9223372036854775809" + * | LOAD-BEARING: proves the strconv.u64tos UNSIGNED + * | route (len 19, first byte '9' not '-') — i64dec + * | would render this negative ("-9223372036854775807", + * | len 20). Also proves wwstage tags it uint (6), not + * | i64, under the widened union. + * int_printf | bsprintf("{}", int 42) -> "42" (formatfield -> + * | formatone -> formatraw placeholder path) + * unaffected_i64 | fprint(42i64) -> "42" (existing arm regression guard) + * unaffected_str | fprint("hi") -> "hi" (existing arm regression guard) + * mixed | fprint(1:int, 2i64, true) -> "1 2 true": int(tag5) + * | + i64(tag0) + bool(tag2) coexist in one call, + * | distinct tags, ww auto-space separator. + * + * Each row builds + runs through BOTH `ww` (cstage) and `ww_ww` + * (wwstage) via the driver (-I lib for the fmt import), then a byte-id + * pass builds the combined.ww via the cstage driver and diffs the asm + * w6c emits against w6c_ww — any cs!=ww tag/render divergence reds here. + * + * BOOTSTRAP-EMBED: fmt is NOT embedded in any selfhost combined.ww — + * w6c/wwdump main.ww never reach wcc/err.ww (the lone `import fmt`), so + * neither selfhost/cmd/{w6c,wwdump}/main.combined.ww carries an fmt fn + * (grep-verified: 0 putbytes/writeone/formatraw; the 2 "formattable" + * hits are cgen comment text). #6 therefore rides NO combined.ww regen. + * This test covers the user-program path, which is the only fmt consumer. + * + * GATE POLARITY: must stay GREEN. A red means the int/uint arm dropped, + * uint mis-routed through the signed renderer (highbit prints negative), + * the wwstage mis-tagged uint as i64, or an existing i64/str arm + * regressed. + */ +#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[] = { + { "bare_int", + "package main;\n" + "import os;\n" + "import fmt;\n" + "import io;\n" + "import memio;\n" + "export fn main() i32 = {\n" + " let buf: [32]u8;\n" + " let st: memio.stream = memio.fixed(buf[0:32]);\n" + " let vs: io.stream = &st.vt;\n" + " let x: int = 42;\n" + " match (fmt.fprint(vs, x)) { case size => {}; case io.error => { return 91; }; };\n" + " let v: str = memio.string(&st);\n" + " if (v.len != 2) { return 92; };\n" + " if (v[0] != 52u8 || v[1] != 50u8) { return 93; };\n" + " return 50;\n" + "};\n", + 50 }, + { "bare_int_neg", + "package main;\n" + "import os;\n" + "import fmt;\n" + "import io;\n" + "import memio;\n" + "export fn main() i32 = {\n" + " let buf: [32]u8;\n" + " let st: memio.stream = memio.fixed(buf[0:32]);\n" + " let vs: io.stream = &st.vt;\n" + " let x: int = -7;\n" + " match (fmt.fprint(vs, x)) { case size => {}; case io.error => { return 91; }; };\n" + " let v: str = memio.string(&st);\n" + " if (v.len != 2) { return 92; };\n" + " if (v[0] != 45u8 || v[1] != 55u8) { return 93; };\n" + " return 51;\n" + "};\n", + 51 }, + { "bare_uint", + "package main;\n" + "import os;\n" + "import fmt;\n" + "import io;\n" + "import memio;\n" + "export fn main() i32 = {\n" + " let buf: [32]u8;\n" + " let st: memio.stream = memio.fixed(buf[0:32]);\n" + " let vs: io.stream = &st.vt;\n" + " let x: uint = 42u64: uint;\n" + " match (fmt.fprint(vs, x)) { case size => {}; case io.error => { return 91; }; };\n" + " let v: str = memio.string(&st);\n" + " if (v.len != 2) { return 92; };\n" + " if (v[0] != 52u8 || v[1] != 50u8) { return 93; };\n" + " return 52;\n" + "};\n", + 52 }, + { "uint_highbit", + "package main;\n" + "import os;\n" + "import fmt;\n" + "import io;\n" + "import memio;\n" + "export fn main() i32 = {\n" + " let buf: [32]u8;\n" + " let st: memio.stream = memio.fixed(buf[0:32]);\n" + " let vs: io.stream = &st.vt;\n" + " let x: uint = 9223372036854775809u64: uint;\n" + " match (fmt.fprint(vs, x)) { case size => {}; case io.error => { return 91; }; };\n" + " let v: str = memio.string(&st);\n" + " if (v.len != 19) { return 92; };\n" + " if (v[0] != 57u8) { return 93; };\n" /* '9', not '-' (45) */ + " if (v[18] != 57u8) { return 94; };\n" /* trailing '9' */ + " return 53;\n" + "};\n", + 53 }, + { "int_printf", + "package main;\n" + "import os;\n" + "import fmt;\n" + "import io;\n" + "export fn main() i32 = {\n" + " let buf: [16]u8;\n" + " let x: int = 42;\n" + " let v: str = \"\";\n" + " match (fmt.bsprintf(buf[0:16], \"{}\", x)) { case let s: str => { v = s; }; case io.error => { return 91; }; };\n" + " if (v.len != 2) { return 92; };\n" + " if (v[0] != 52u8 || v[1] != 50u8) { return 93; };\n" + " return 54;\n" + "};\n", + 54 }, + { "unaffected_i64", + "package main;\n" + "import os;\n" + "import fmt;\n" + "import io;\n" + "import memio;\n" + "export fn main() i32 = {\n" + " let buf: [32]u8;\n" + " let st: memio.stream = memio.fixed(buf[0:32]);\n" + " let vs: io.stream = &st.vt;\n" + " match (fmt.fprint(vs, 42i64)) { case size => {}; case io.error => { return 91; }; };\n" + " let v: str = memio.string(&st);\n" + " if (v.len != 2) { return 92; };\n" + " if (v[0] != 52u8 || v[1] != 50u8) { return 93; };\n" + " return 55;\n" + "};\n", + 55 }, + { "unaffected_str", + "package main;\n" + "import os;\n" + "import fmt;\n" + "import io;\n" + "import memio;\n" + "export fn main() i32 = {\n" + " let buf: [32]u8;\n" + " let st: memio.stream = memio.fixed(buf[0:32]);\n" + " let vs: io.stream = &st.vt;\n" + " match (fmt.fprint(vs, \"hi\")) { case size => {}; case io.error => { return 91; }; };\n" + " let v: str = memio.string(&st);\n" + " if (v.len != 2) { return 92; };\n" + " if (v[0] != 104u8 || v[1] != 105u8) { return 93; };\n" + " return 56;\n" + "};\n", + 56 }, + { "mixed", + "package main;\n" + "import os;\n" + "import fmt;\n" + "import io;\n" + "import memio;\n" + "export fn main() i32 = {\n" + " let buf: [32]u8;\n" + " let st: memio.stream = memio.fixed(buf[0:32]);\n" + " let vs: io.stream = &st.vt;\n" + " let x: int = 1;\n" + " match (fmt.fprint(vs, x, 2i64, true)) { case size => {}; case io.error => { return 91; }; };\n" + " let v: str = memio.string(&st);\n" + " if (v.len != 8) { return 92; };\n" /* "1 2 true" */ + " if (v[0] != 49u8 || v[1] != 32u8) { return 93; };\n" /* '1' ' ' */ + " if (v[2] != 50u8 || v[3] != 32u8) { return 94; };\n" /* '2' ' ' */ + " if (v[4] != 116u8 || v[7] != 101u8) { return 95; };\n" /* 't' .. 'e' */ + " return 57;\n" + "};\n", + 57 }, +}; + +static int +write_source(const char *path, const char *src) +{ + FILE *f = fopen(path, "wb"); + if (!f) return -1; + fputs(src, f); + fclose(f); + return 0; +} + +/* Per-row tmpdir cleanup. ww_ww writes intermediates next to the source + * (task #15), so each row's build leaves .{combined.ww,s,o} + bare + * exe alongside. Mirror of 781's. */ +static void +cleanup_tmp(const char *tmpdir, const char *base) +{ + char p[640]; + snprintf(p, sizeof p, "%s/%s.ww", tmpdir, base); unlink(p); + snprintf(p, sizeof p, "%s/%s.s", tmpdir, base); unlink(p); + snprintf(p, sizeof p, "%s/%s.o", tmpdir, base); unlink(p); + snprintf(p, sizeof p, "%s/%s.combined.ww", tmpdir, base); unlink(p); + snprintf(p, sizeof p, "%s/%s", tmpdir, base); unlink(p); + rmdir(tmpdir); +} + +static int +build_via_driver(const char *driver, const char *tmpdir, const char *cwd, + const char *src) +{ + char cmd[2048]; + snprintf(cmd, sizeof cmd, + "cd %s && timeout 180 %s build -I %s/lib %s 2>/dev/null", + tmpdir, driver, cwd, src); + return runwait(cmd); +} + +static int +run_row(const char *driver, const char *cwd, const struct row *r, int seq) +{ + char tmpdir[256], src[512], base[64], outbin[768]; + snprintf(tmpdir, sizeof tmpdir, "/tmp/fic_%d_d_%d", getpid(), seq); + snprintf(base, sizeof base, "main815"); + snprintf(src, sizeof src, "%s/%s.ww", tmpdir, base); + mkdir(tmpdir, 0755); + if (write_source(src, r->src) != 0) { cleanup_tmp(tmpdir, base); return -1; } + int rc; + int br = build_via_driver(driver, tmpdir, cwd, src); + if (br == 0) { + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + rc = runwait(outbin); + } else { + rc = -1; + } + cleanup_tmp(tmpdir, base); + return rc; +} + +/* asm_byte_identical — build combined.ww via the cstage driver (needs + * the -I lib fmt import resolution that raw w6c can't do), then compile + * that combined.ww with both w6c and w6c_ww and diff the asm. Proves no + * cs!=ww tag/render divergence on the widened union. */ +static int +asm_byte_identical(const char *bin, const char *cdrv, const char *cwd, + const struct row *r, int seq) +{ + char tmpdir[256], base[64], src[512], combined[640], cs[640], ws[640]; + char cmd[2048]; + snprintf(tmpdir, sizeof tmpdir, "/tmp/fic_%d_b_%d", getpid(), seq); + snprintf(base, sizeof base, "main815b"); + snprintf(src, sizeof src, "%s/%s.ww", tmpdir, base); + snprintf(combined, sizeof combined, "%s/%s.combined.ww", tmpdir, base); + snprintf(cs, sizeof cs, "%s/%s_c.s", tmpdir, base); + snprintf(ws, sizeof ws, "%s/%s_w.s", tmpdir, base); + mkdir(tmpdir, 0755); + if (write_source(src, r->src) != 0) { cleanup_tmp(tmpdir, base); return -1; } + + snprintf(cmd, sizeof cmd, + "cd %s && timeout 180 %s build -I %s/lib %s 2>/dev/null", + tmpdir, cdrv, cwd, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "fmt_int_run[byteid][%s]: combined.ww build failed\n", + r->label); + cleanup_tmp(tmpdir, base); + return -1; + } + + int rc = 0; + snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s 2>/dev/null", bin, cs, combined); + if (runwait(cmd) != 0) { + fprintf(stderr, "fmt_int_run[byteid][%s]: w6c errored\n", r->label); + rc = -1; + } + if (rc == 0) { + snprintf(cmd, sizeof cmd, "%s/w6c_ww -o %s %s 2>/dev/null", + bin, ws, combined); + if (runwait(cmd) != 0) { + fprintf(stderr, "fmt_int_run[byteid][%s]: w6c_ww errored\n", + r->label); + rc = -1; + } + } + if (rc == 0) { + FILE *fc = fopen(cs, "rb"); + FILE *fw = fopen(ws, "rb"); + if (!fc || !fw) { + rc = -1; + } else { + for (;;) { + int a = fgetc(fc); + int b = fgetc(fw); + if (a != b) { rc = -1; break; } + if (a == EOF) break; + } + } + if (fc) fclose(fc); + if (fw) fclose(fw); + if (rc != 0) + fprintf(stderr, + "fmt_int_run[byteid][%s]: cstage vs wwstage asm differs\n", + r->label); + } + + unlink(cs); unlink(ws); + cleanup_tmp(tmpdir, base); + return rc; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char cwd[256]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + char absbin[512]; + if (bin[0] != '/') { + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char cdrv[640], wdrv[640]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + int wwpresent = (access(wdrv, X_OK) == 0); + int seq = 0; + + for (int i = 0; i < n; i++) { + total++; + int got = run_row(cdrv, cwd, &rows[i], seq++); + if (got != rows[i].want_exit) { + fprintf(stderr, "fmt_int_run[cs][%s]: exit=%d want=%d\n", + rows[i].label, got, rows[i].want_exit); + fail++; + } + if (wwpresent) { + total++; + int gw = run_row(wdrv, cwd, &rows[i], seq++); + if (gw != rows[i].want_exit) { + fprintf(stderr, "fmt_int_run[ww][%s]: exit=%d want=%d\n", + rows[i].label, gw, rows[i].want_exit); + fail++; + } + } + } + + if (wwpresent) { + for (int i = 0; i < n; i++) { + total++; + if (asm_byte_identical(bin, cdrv, cwd, &rows[i], seq++) != 0) + fail++; + } + } else { + fprintf(stderr, "fmt_int_run: skip wwstage (no %s)\n", wdrv); + } + + if (fail) { + fprintf(stderr, "fmt_int_run: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("fmt_int_run: %d/%d ok\n", total, total); + return 0; +}