From a3f3153943e8d8b150856030b486a6f61d181f2e Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 29 May 2026 11:50:55 +0900 Subject: [PATCH] lib/fmt: port V-side modifier formatting (#94 fold-e6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit V's vfprintf parsed mods via scanmods but dropped them after parse — vformatfield routed straight to vwriteone (no width / alignment / pad / sign / base / prec honoured). Port the OLD modifier path (fmt.ww: 443-641 rawlen* / formatraw / formatone) into vstream.ww as the v* twins, widen vformatfield to take *mods, and pass &m through vfprintf at the call site. The v* helpers mirror OLD verbatim (compute body identical; vputbytes + (size | io.error) routing replacing putbytes + (i32 | io.closed)); shared compute helpers (signof / digitsu64 / basenum) and modifier enums (neg / alignment / mods) are reused directly from fmt.ww via package scope. fmt.ww UNCHANGED — fold-eFinal (#50) collapses both surfaces and dedupes the rawlen-family. drew NaN/Inf signoff: strconv.f64tos / f32tos already render "nan"/"infinity" with no leading '-', so the sign-peel in vrawlenf64 + vformatraw f64 arm is a no-op on those views (same OLD path at fmt.ww:520-562). ken cs==ww mechanical: both stages compile the new V-side identically; 990-997 byte-id gates + combined_ww_fresh stay green (fmt is not embedded in any selfhost main.combined.ww — grep verified pre-impl). test/wcc/780_fmt_vstream_mods_run.c (cstage-only per #209): 5 rows covering width / precision / base_hex / sign_plus / zero_pad. STAGE_WW blocked by #209 (wwstage formattable match-arm bail), same carve-out as 777_fmt_vstream_run. --- Makefile | 8 + lib/fmt/vstream.ww | 254 +++++++++++++++++++- test/wcc/780_fmt_vstream_mods_run.c | 355 ++++++++++++++++++++++++++++ 3 files changed, 604 insertions(+), 13 deletions(-) create mode 100644 test/wcc/780_fmt_vstream_mods_run.c diff --git a/Makefile b/Makefile index cba435ce..4f5aede5 100644 --- a/Makefile +++ b/Makefile @@ -328,6 +328,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_io_vtable_run \ $(BIN)/test_memio_vstream_run \ $(BIN)/test_fmt_vstream_run \ + $(BIN)/test_fmt_vstream_mods_run \ $(BIN)/test_bufio_vstream_run \ $(BIN)/test_log_vstream_run \ $(BIN)/test_use_promote_alias \ @@ -706,6 +707,13 @@ $(BIN)/test_fmt_vstream_run: test/wcc/777_fmt_vstream_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_fmt_vstream_mods_run: test/wcc/780_fmt_vstream_mods_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/fmt/vstream.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/vstream.ww b/lib/fmt/vstream.ww index 39e3ffd6..2188ebe8 100644 --- a/lib/fmt/vstream.ww +++ b/lib/fmt/vstream.ww @@ -39,7 +39,15 @@ // because the OLD versions take `*io.stream` (the legacy struct) and // fmt.ww must stay UNCHANGED this fold. Shared bits — `i64dec`, // `modsinit`, `scandigits`, `scanmods`, `formattable`, `field`, -// `mods`, `fmtabort` — are reused directly from fmt.ww (same package). +// `mods`, `fmtabort`, `signof`, `digitsu64`, `basenum`, the modifier +// enums (`neg`, `alignment`) — are reused directly from fmt.ww (same +// package). The v* modifier-formatting helpers (vrawleni64 / vrawlenstr +// / vrawlenf64 / vrawlen / vformatraw / vformatone) mirror the OLD +// rawlen* / formatraw / formatone verbatim (compute body identical; +// vputbytes + (size | io.error) routing instead of putbytes + +// (i32 | io.closed)) — #94 fold-e6 ports them V-side so V's printf +// honours modifiers (fold-e3 dropped them after parse). eFinal (#50) +// collapses both surfaces. // // Sibling tasks parked here (filed, NOT fixed): // @@ -136,33 +144,253 @@ fn vwriteone(vs: io.vstream, a: formattable) (size | io.error) = { return 0: size; }; -// vformatfield — field→formattable inline-per-arm dispatch. The -// for-loop call site in vfprintf would trip task #18 (silent -// miscompile of 24B return-by-value in for-loop context) if widened -// via a helper; inline-per-arm sidesteps it — same shape OLD +// ---- V-side modifier formatting (port of OLD rawlen*/formatraw/ +// formatone from fmt.ww:443-641). Project #94 fold-e6. +// +// V-side mirror of the OLD modifier-formatting machinery so the V +// printf path honours width / alignment / pad / sign / base / prec +// instead of dropping mods after parse. Drew-signoff: NaN/Inf safe +// — strconv.f64tos / f32tos render "nan"/"infinity" with no leading +// '-', so the sign-peel in vrawlenf64 / vformatraw f64 arm is a +// no-op on those views (sibling of OLD rawlenf64's same path). Ken- +// signoff: cs==ww mechanical — both stages compile the new V-side +// identically; eFinal (#50) graduates the V-side over the OLD. +// +// Helpers mirror fmt.ww verbatim (compute-only, no I/O) rather than +// reusing OLD by call — fold-eFinal (#50) atomically deletes the OLD +// surface AND collapses these v* names back, so the duplication is +// transient. Mirror sites: +// +// vrawleni64 fmt.ww:443 rawleni64 +// vrawlenstr fmt.ww:457 rawlenstr +// vrawlenf64 fmt.ww:569 rawlenf64 +// vrawlen fmt.ww:585 rawlen +// vformatraw fmt.ww:465 formatraw +// vformatone fmt.ww:598 formatone + +// vrawleni64 — bytes the raw render of `v` under `m` would emit; the +// signof / digitsu64 / basenum helpers are read from fmt.ww (same +// package). Mirror fmt.ww:443. +fn vrawleni64(v: i64, m: *mods) i32 = { + let neg_flag: bool = v < 0; + let u: u64 = v: u64; + if (neg_flag) { u = (-v): u64; }; + let signlen: i32 = 0; + if (signof(neg_flag, m) != 0u8) { signlen = 1; }; + let dlen: i32 = digitsu64(u, basenum(m.base)); + let inner: i32 = dlen; + if (m.prec > signlen + dlen) { inner = m.prec - signlen; }; + return signlen + inner; +}; + +// vrawlenstr — bytes the raw render of `s` would emit (after `prec` +// truncation). Mirror fmt.ww:457. +fn vrawlenstr(s: str, m: *mods) i32 = { + if (m.prec > 0 && m.prec < s.len) { return m.prec; }; + return s.len; +}; + +// vrawlenf64 — bytes the raw render of `v` under `m` would emit; the +// strconv.f64tos static-buffer view is consumed by reading view.len +// + view.ptr[0] before the sign byte is folded into signof. Mirror +// fmt.ww:569; drew NaN/Inf safe (strconv emits no leading '-' on +// nan/inf, so the peel is a no-op on those views). +fn vrawlenf64(v: f64, m: *mods) i32 = { + let view: str = strconv.f64tos(v); + let body: i32 = view.len; + let had_neg: bool = false; + if (body > 0 && view.ptr[0] == 45u8) { + had_neg = true; + body -= 1; + }; + let signlen: i32 = 0; + if (signof(had_neg, m) != 0u8) { signlen = 1; }; + return signlen + body; +}; + +// vrawlen — dispatch the rawlen sum over formattable. Mirror fmt.ww:585. +fn vrawlen(arg: formattable, m: *mods) i32 = { + match (arg) { + case let v: i64 => return vrawleni64(v, m); + case let v: str => return vrawlenstr(v, m); + case let b: bool => { if (b) { return 4; }; return 5; }; + case let r: rune => return 1; + case let v: f64 => return vrawlenf64(v, m); + }; + return 0; // unreachable — match is exhaustive +}; + +// vformatraw — write the bare value (no width padding) to `vs`. Mirror +// fmt.ww:465 formatraw, vputbytes-routed and (size | io.error)-typed. +// `prec` ignored on f64 (strconv.f64tos is shortest-G with no precision +// knob); base ignored on f64 (Hare too — base is int-only). Drew- +// signoff: NaN/Inf strings emitted unchanged via vputbytes. +fn vformatraw(vs: io.vstream, arg: formattable, m: *mods) (size | io.error) = { + match (arg) { + case let v: 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) = vputbytes(vs, &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] = 48u8; // '0' + let r: (size | io.error) = vputbytes(vs, &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) = vputbytes(vs, view.ptr, view.len); + match (r) { + case let n: size => { total += n; }; + case let e: io.error => return e; + }; + return total; + }; + case let v: str => { + let n: i32 = vrawlenstr(v, m); + return vputbytes(vs, v.ptr, n); + }; + case let b: bool => { + let v: str = "false"; + if (b) { v = "true"; }; + return vputbytes(vs, v.ptr, v.len); + }; + case let r: rune => { + let buf: [4]u8; + buf[0] = r: u8; + return vputbytes(vs, &buf[0], 1); + }; + case let v: f64 => { + // strconv.f64tos prepends '-' for negative values; peel + // here so signof folds neg/plus/space mods uniformly with + // the i64 arm. Drew NaN/Inf signoff: nan/infinity views + // have no leading '-', so this peel is a no-op for them + // (sign-mod on a nan emits e.g. "+nan" — a fmt-layer edge, + // not strconv's; same OLD behaviour at fmt.ww:520-562). + let view: str = strconv.f64tos(v); + let neg_flag: bool = false; + if (view.len > 0 && view.ptr[0] == 45u8) { // '-' + neg_flag = true; + view.ptr = view.ptr + 1u64; + view.len -= 1; + }; + 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) = vputbytes(vs, &buf[0], 1); + match (r) { + case let n: size => { total += n; }; + case let e: io.error => return e; + }; + }; + let r: (size | io.error) = vputbytes(vs, 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 +}; + +// vformatone — render `arg` to `vs` with `m`'s width / alignment / pad +// applied. Mirror fmt.ww:598 formatone; the tail-pad loop drives on +// a counter (mirrors OLD) because vputbytes over memio.fixed reports +// a full buffer as a 0-byte partial write, not io.error — looping on +// `total < m.width` would spin on bsprintf_v when the sink runs out. +fn vformatone(vs: io.vstream, arg: formattable, m: *mods) (size | io.error) = { + let start: i32 = 0; + if (m.width > 0 && m.alignment != alignment.LEFT) { + let raw: i32 = vrawlen(arg, m); + let pad: i32 = 0; + if (raw < m.width) { pad = m.width - raw; }; + if (m.alignment == alignment.CENTER) { start = (pad + 1) / 2; } + else { start = pad; }; + }; + let total: size = 0; + let i: i32 = 0; + let padb: [1]u8; + padb[0] = m.pad: u8; + for (i < start) { + let r: (size | io.error) = vputbytes(vs, &padb[0], 1); + match (r) { + case let n: size => { total += n; }; + case let e: io.error => return e; + }; + i += 1; + }; + let r1: (size | io.error) = vformatraw(vs, arg, m); + match (r1) { + case let n: size => { total += n; }; + case let e: io.error => return e; + }; + let need: i32 = 0; + let twidth: size = m.width: size; + if (twidth > total) { need = (twidth - total): i32; }; + let j: i32 = 0; + for (j < need) { + let r: (size | io.error) = vputbytes(vs, &padb[0], 1); + match (r) { + case let n: size => { total += n; }; + case let e: io.error => return e; + }; + j += 1; + }; + return total; +}; + +// vformatfield — field→formattable inline-per-arm dispatch through +// vformatone. The for-loop call site in vfprintf would trip task #18 +// (silent miscompile of 24B return-by-value in for-loop context) if +// widened via a helper; inline-per-arm sidesteps it — same shape OLD // formatfield uses at fmt.ww:648. `*mods` arm aborts (parametric '%' -// form not implemented; OLD fprintf aborts identically). -fn vformatfield(vs: io.vstream, f: field) (size | io.error) = { +// form not implemented; OLD fprintf aborts identically). #94 fold-e6 +// widens the signature with `*mods` so the V path honours modifiers +// (fold-e3 dropped them after parse). +fn vformatfield(vs: io.vstream, f: field, m: *mods) (size | io.error) = { match (f) { case let v: i64 => { let a: formattable = v; - return vwriteone(vs, a); + return vformatone(vs, a, m); }; case let v: str => { let a: formattable = v; - return vwriteone(vs, a); + return vformatone(vs, a, m); }; case let b: bool => { let a: formattable = b; - return vwriteone(vs, a); + return vformatone(vs, a, m); }; case let r: rune => { let a: formattable = r; - return vwriteone(vs, a); + return vformatone(vs, a, m); }; case let v: f64 => { let a: formattable = v; - return vwriteone(vs, a); + return vformatone(vs, a, m); }; case let p: *mods => { fmtabort(); let z: size = 0; return z; }; }; @@ -236,7 +464,7 @@ export fn vfprintf(vs: io.vstream, fmt: str, args: field...) (size | io.error) = if (i >= fmt.len || fmt[i] != 125u8) { fmtabort(); }; i += 1; if (idx >= args.len) { fmtabort(); }; - let r: (size | io.error) = vformatfield(vs, args[idx]); + let r: (size | io.error) = vformatfield(vs, args[idx], &m); match (r) { case let n: size => { total += n; }; case let e: io.error => return e; diff --git a/test/wcc/780_fmt_vstream_mods_run.c b/test/wcc/780_fmt_vstream_mods_run.c new file mode 100644 index 00000000..bde4f692 --- /dev/null +++ b/test/wcc/780_fmt_vstream_mods_run.c @@ -0,0 +1,355 @@ +/* + * 780_fmt_vstream_mods_run — project #94 fold-e6 sentinel. Pins the + * V-side modifier-formatting machinery (vrawleni64 / vrawlenstr / + * vrawlenf64 / vrawlen / vformatraw / vformatone) in lib/fmt/ + * vstream.ww. Before fold-e6, the V printf path parsed mods via + * scanmods but dropped them after parse: vformatfield routed straight + * to vwriteone (no width / alignment / pad / sign / base / prec + * honoured). fold-e6 widens vformatfield to take `*mods` and routes + * through vformatone — same dispatch shape as OLD formatfield at + * fmt.ww:648. Coexists with the OLD modifier path at fmt.ww:443-641 + * (fmt.ww UNCHANGED this fold); fold-eFinal (task #50) collapses both + * surfaces. + * + * Each row imports fmt + os + io, opens a per-process /tmp output + * file, calls fdprintf_v with a {:mods} format string + one arg, and + * asserts the exact byte content read back. + * + * Cstage-only per row — pre-existing wwstage bug #209 (sibling of + * #190): the wwstage checker bails on fmt.formattable match-arms + * whenever a downstream probe `import fmt;`s the package. Identical + * carve-out to 777_fmt_vstream_run (cited there at line 18-33). + * Byte-id graduates with #209 close. + * + * row | format | arg | expect | exit + * -----------------+------------+--------------+-----------+----- + * width | "{:5}" | 42i64 | " 42" | 50 + * precision | "{:.3}" | "hello" | "hel" | 51 + * base_hex | "{:x}" | 255i64 | "ff" | 52 + * sign_plus | "{:+}" | 7i64 | "+7" | 53 + * zero_pad | "{:_05}" | 42i64 | "00042" | 54 + * + * Modifier surface exercised (subset of scanmods at fmt.ww:376-405): + * + * width digits 1..9 — RIGHT-align pad to N chars + * prec '.' + digits — str truncation / int min-digits + * base 'x' — strconv.base.HEX_LOWER + * neg '+' — emit '+' on non-negative + * pad '_' — pad rune override (default space when ':') + * + * Drew NaN/Inf signoff (carry from task #58): f64 arm not exercised + * here (rows pin i64+str+bool fastpaths); follow-on fold can add f64 + * once Drew's strconv.f64tos shortest-G stays static. The fold-e6 + * implementation honours the sign-peel in vformatraw f64 arm; an + * end-to-end test lands when a NaN/Inf-aware probe joins the corpus. + * + * IMPORT-ORDER WORKAROUND (sibling task #208, pre-existing): every + * row places `import os;` FIRST for the same reason 776/777 document + * (wwstage checker ordering-sensitivity on os.tryread/trywrite/ + * tryopen's bare `return r;` over a tagged return type). + * + * SIBLINGS (filed inline, NOT fixed here — fold-e6 is purely additive + * over fold-e1/e2/e3/e4/e5's frozen io.* + memio.* + fmt.* + bufio.* + * + log.* surface): + * + * - #209 (wwstage formattable match-arm bail): blocks STAGE_WW + * here, same as 777_fmt_vstream_run. Cstage-only until close. + * - eFinal (#50): collapses V + OLD surfaces; the `v*` names rename + * off and the OLD rawlen* / formatraw / formatone delete. The v* + * duplication of the rawlen-family today is transient — eFinal + * dedupes. + * + * BOOTSTRAP-EMBED CHECK: fmt is NOT embedded in any selfhost + * combined.ww (grep confirmed pre-impl). Adding modifier formatting + * to lib/fmt/vstream.ww does NOT require a Makefile regen (#110); + * only test paths see the new machinery. 990-997 byte-id gates stay + * green by virtue of fmt being test-only on the bootstrap surface. + * + * GATE POLARITY: must stay GREEN. A red here means vformatone / + * vformatraw / vrawlen miscomputed bytes, vformatfield regressed its + * *mods route, scanmods/modsinit (shared from fmt.ww) drifted, or + * the strconv.u64tos base path miscompiled under fdprintf_v's + * intrusive vstream→*fd_ctx dispatch. + */ +#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; +} + +#define STAGE_CS 1 +#define STAGE_WW 2 + +struct row { + const char *label; + const char *src; + int want_exit; + int stage_mask; + int byte_id; +}; + +static const struct row rows[] = { + { "width", + "package main;\n" + "import os;\n" + "import fmt;\n" + "import io;\n" + "export fn main() i32 = {\n" + " let path: str = \"/tmp/wwfmtv_780_a\";\n" + " let fd: i32 = os.open(path, os.flag.RDWR | os.flag.CREATE | os.flag.TRUNC, 0o644: i32);\n" + " if (fd < 0) { return 90; };\n" + " let wr = fmt.fdprintf_v(fd, \"{:5}\", 42i64);\n" + " let nw: size = 0;\n" + " match (wr) {\n" + " case let n: size => { nw = n; };\n" + " case io.error => { return 91; };\n" + " };\n" + " os.close(fd);\n" + " let rd: i32 = os.open(path, os.flag.RDONLY, 0i32);\n" + " if (rd < 0) { return 92; };\n" + " let buf: [16]u8;\n" + " let n: i64 = os.read(rd, &buf[0], 16u64);\n" + " os.close(rd);\n" + " if (n != 5i64) { return 93; };\n" + " if (nw != 5: size) { return 94; };\n" + " if (buf[0] != 32u8 || buf[1] != 32u8 || buf[2] != 32u8 || buf[3] != 52u8 || buf[4] != 50u8) { return 95; };\n" + " return 50;\n" + "};\n", + 50, + STAGE_CS, 0 }, + { "precision", + "package main;\n" + "import os;\n" + "import fmt;\n" + "import io;\n" + "export fn main() i32 = {\n" + " let path: str = \"/tmp/wwfmtv_780_b\";\n" + " let fd: i32 = os.open(path, os.flag.RDWR | os.flag.CREATE | os.flag.TRUNC, 0o644: i32);\n" + " if (fd < 0) { return 90; };\n" + " let wr = fmt.fdprintf_v(fd, \"{:.3}\", \"hello\");\n" + " let nw: size = 0;\n" + " match (wr) {\n" + " case let n: size => { nw = n; };\n" + " case io.error => { return 91; };\n" + " };\n" + " os.close(fd);\n" + " let rd: i32 = os.open(path, os.flag.RDONLY, 0i32);\n" + " if (rd < 0) { return 92; };\n" + " let buf: [16]u8;\n" + " let n: i64 = os.read(rd, &buf[0], 16u64);\n" + " os.close(rd);\n" + " if (n != 3i64) { return 93; };\n" + " if (nw != 3: size) { return 94; };\n" + " if (buf[0] != 104u8 || buf[1] != 101u8 || buf[2] != 108u8) { return 95; };\n" + " return 51;\n" + "};\n", + 51, + STAGE_CS, 0 }, + { "base_hex", + "package main;\n" + "import os;\n" + "import fmt;\n" + "import io;\n" + "export fn main() i32 = {\n" + " let path: str = \"/tmp/wwfmtv_780_c\";\n" + " let fd: i32 = os.open(path, os.flag.RDWR | os.flag.CREATE | os.flag.TRUNC, 0o644: i32);\n" + " if (fd < 0) { return 90; };\n" + " let wr = fmt.fdprintf_v(fd, \"{:x}\", 255i64);\n" + " let nw: size = 0;\n" + " match (wr) {\n" + " case let n: size => { nw = n; };\n" + " case io.error => { return 91; };\n" + " };\n" + " os.close(fd);\n" + " let rd: i32 = os.open(path, os.flag.RDONLY, 0i32);\n" + " if (rd < 0) { return 92; };\n" + " let buf: [16]u8;\n" + " let n: i64 = os.read(rd, &buf[0], 16u64);\n" + " os.close(rd);\n" + " if (n != 2i64) { return 93; };\n" + " if (nw != 2: size) { return 94; };\n" + " if (buf[0] != 102u8 || buf[1] != 102u8) { return 95; };\n" + " return 52;\n" + "};\n", + 52, + STAGE_CS, 0 }, + { "sign_plus", + "package main;\n" + "import os;\n" + "import fmt;\n" + "import io;\n" + "export fn main() i32 = {\n" + " let path: str = \"/tmp/wwfmtv_780_d\";\n" + " let fd: i32 = os.open(path, os.flag.RDWR | os.flag.CREATE | os.flag.TRUNC, 0o644: i32);\n" + " if (fd < 0) { return 90; };\n" + " let wr = fmt.fdprintf_v(fd, \"{:+}\", 7i64);\n" + " let nw: size = 0;\n" + " match (wr) {\n" + " case let n: size => { nw = n; };\n" + " case io.error => { return 91; };\n" + " };\n" + " os.close(fd);\n" + " let rd: i32 = os.open(path, os.flag.RDONLY, 0i32);\n" + " if (rd < 0) { return 92; };\n" + " let buf: [16]u8;\n" + " let n: i64 = os.read(rd, &buf[0], 16u64);\n" + " os.close(rd);\n" + " if (n != 2i64) { return 93; };\n" + " if (nw != 2: size) { return 94; };\n" + " if (buf[0] != 43u8 || buf[1] != 55u8) { return 95; };\n" + " return 53;\n" + "};\n", + 53, + STAGE_CS, 0 }, + { "zero_pad", + "package main;\n" + "import os;\n" + "import fmt;\n" + "import io;\n" + "export fn main() i32 = {\n" + " let path: str = \"/tmp/wwfmtv_780_e\";\n" + " let fd: i32 = os.open(path, os.flag.RDWR | os.flag.CREATE | os.flag.TRUNC, 0o644: i32);\n" + " if (fd < 0) { return 90; };\n" + " let wr = fmt.fdprintf_v(fd, \"{:_05}\", 42i64);\n" + " let nw: size = 0;\n" + " match (wr) {\n" + " case let n: size => { nw = n; };\n" + " case io.error => { return 91; };\n" + " };\n" + " os.close(fd);\n" + " let rd: i32 = os.open(path, os.flag.RDONLY, 0i32);\n" + " if (rd < 0) { return 92; };\n" + " let buf: [16]u8;\n" + " let n: i64 = os.read(rd, &buf[0], 16u64);\n" + " os.close(rd);\n" + " if (n != 5i64) { return 93; };\n" + " if (nw != 5: size) { return 94; };\n" + " if (buf[0] != 48u8 || buf[1] != 48u8 || buf[2] != 48u8 || buf[3] != 52u8 || buf[4] != 50u8) { return 95; };\n" + " return 54;\n" + "};\n", + 54, + STAGE_CS, 0 }, +}; + +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 (filed task #15), so each row's build leaves + * .{combined.ww,s,o} + bare exe alongside. Mirror of 777'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/fvm_%d_d_%d", getpid(), seq); + snprintf(base, sizeof base, "main780"); + 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; +} + +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++) { + if (rows[i].stage_mask & STAGE_CS) { + total++; + int got = run_row(cdrv, cwd, &rows[i], seq++); + if (got != rows[i].want_exit) { + fprintf(stderr, + "fmt_vstream_mods_run[cs][%s]: exit=%d want=%d\n", + rows[i].label, got, rows[i].want_exit); + fail++; + } + } + if (wwpresent && (rows[i].stage_mask & STAGE_WW)) { + total++; + int got = run_row(wdrv, cwd, &rows[i], seq++); + if (got != rows[i].want_exit) { + fprintf(stderr, + "fmt_vstream_mods_run[ww][%s]: exit=%d want=%d\n", + rows[i].label, got, rows[i].want_exit); + fail++; + } + } + } + + if (!wwpresent) + fprintf(stderr, "fmt_vstream_mods_run: skip wwstage (no %s)\n", wdrv); + + if (fail) { + fprintf(stderr, "fmt_vstream_mods_run: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("fmt_vstream_mods_run: %d/%d ok\n", total, total); + return 0; +}