diff --git a/Makefile b/Makefile index a813e8ff..21d3a1f2 100644 --- a/Makefile +++ b/Makefile @@ -327,6 +327,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_tagged_widen_named_variant \ $(BIN)/test_io_vtable_run \ $(BIN)/test_memio_vstream_run \ + $(BIN)/test_fmt_vstream_run \ $(BIN)/test_use_promote_alias \ $(BIN)/test_field_signed $(BIN)/test_frame_argcount \ $(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \ @@ -696,6 +697,13 @@ $(BIN)/test_memio_vstream_run: test/wcc/776_memio_vstream_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_fmt_vstream_run: test/wcc/777_fmt_vstream_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_arrlit_str_full: test/wcc/711_arrlit_str_full.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 new file mode 100644 index 00000000..4838e4c7 --- /dev/null +++ b/lib/fmt/vstream.ww @@ -0,0 +1,327 @@ +// vstream — Hare-shaped vtable wrappers over an fd sink. Project #94 +// fold-e3 (Option C, parallel API for lib/fmt). +// +// Adds four wrappers that take a raw fd and dispatch through an +// [[io.vstream]] (= `*io.vtable`, the Hare-shape from +// lib/io/stream.ww) alongside the pre-vtable fdprint / fdprintln / +// fdprintf / fdprintfln in fmt.ww. The OLD surface stays untouched +// here — fold-eFinal (task #50) atomically flips the package shape: +// deletes the OLD wrappers + callbacks, renames `_v` suffix off, and +// migrates the few callers. +// +// Hare routes fmt's fd sinks through `io::handle = (io::file | int)` +// (ref/hare/fmt/wrappers.ha:9-25); ww doesn't have the handle sum yet +// so the pure-vstream sink — drew-deferred per fold-d/fold-e3 — +// stands in until io fold-2 lands the handle port. fd_ctx is the +// minimum carrier: one tagged field (vt) + one i32 (fd). Single- +// tagged-field shape lets direct struct-lit init through (sibling +// task #207 only bites multi-tagged-field copies); ken's escape-risk +// discipline holds — every wrapper stack-allocates fd_ctx inside its +// own frame, gets `vs = &c.vt` from the same frame, and dispatches +// without returning &c outside it. +// +// Cast workaround per #206: bare `&fn_name` does not type-check as a +// `(* | void)` field-init / let-binding. Explicit +// `(&fn_name): *io.` cast at each store site is the Hare- +// faithful minimum-touch route — same workaround memio/vstream.ww +// uses (2 cast tokens here at the vtable wire-up site; both drop out +// wholesale once #206 closes). +// +// Dispatch chain (per wrapper): +// fd{print,println,printf,printfln}_v +// → vfprint / vfprintf (internal vstream-side formatter) +// → io.st_write(vs, buf) +// → vs.writer (= fdsinkwrite_v after the cast) +// → os.write(c.fd, ...) +// +// The internal vfprint/vfprintf duplicate the per-arg and {n}- +// placeholder loops from fmt.ww (fprint at line 177, fprintf at 676) +// 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). +// +// Sibling tasks parked here (filed, NOT fixed): +// +// - io fold-2 (handle port): drew-deferred. Hare's +// ref/hare/fmt/wrappers.ha:9-25 routes through io::handle; +// fdNNN_v collapses into bare fNNN_v once handle = (file | int) +// lands and the V API graduates over the OLD per-fd surface. +// - #206 (bare &fn → (*alias|void)): 2 cast sites here; drops +// out wholesale on close. +// - #173 (TRY-on-tagged-return both-stages broken): fdsinkwrite_v +// constructs nomem and widens to io.error explicitly rather +// than using `os.trywrite(...)?`; same shape memio.vstream.ww +// adopted at line 71-74. + +package fmt; + +import io; +import os; +import strconv; + +// fd_ctx — vt at offset 0 for the intrusive vstream→*fd_ctx cast. +// Mirrors lib/memio.fixed_ctx (memio/vstream.ww:48); single tagged +// field (vt) means the multi-tagged-field drop in #207 doesn't bite. +export type fd_ctx = struct { + vt: io.vtable, + fd: i32, +}; + +// fdsinkread_v — eof-only sink; the fd half of fmt is write-only. +// Mirrors OLD fdsinkread (fmt.ww:765). The unused-`buf` parameter +// matches io.reader's signature. +fn fdsinkread_v(s: io.vstream, buf: []u8) (size | io.eof | io.error) = { + let e: io.eof; + return e; +}; + +// fdsinkwrite_v — recover fd via intrusive cast, dispatch one +// os.write. -errno collapses to a nomem-widened io.error: the OLD +// fdsinkwrite (fmt.ww:768) flattens this to io.closed; we keep the +// Hare-shape error here because the V surface returns io.error +// directly. Construction-then-widen (vs `os.trywrite(...)?`) +// sidesteps #173 same as memio.vstream.ww (line 71-74 note). +fn fdsinkwrite_v(s: io.vstream, buf: []u8) (size | io.error) = { + let c: *fd_ctx = s: *fd_ctx; + let r: i64 = os.write(c.fd, buf.ptr, buf.len: u64); + if (r < 0) { + let nm: nomem; + let e: io.error = nm; + return e; + }; + return r: size; +}; + +// ---- internal vstream-side formatters ------------------------------- + +// vputbytes — io.st_write(vs, [ptr..ptr+n)). Internal helper; the +// inline `let v` slice synthesis mirrors fmt.putbytes (fmt.ww:160) +// for the legacy *io.stream sink. +fn vputbytes(vs: io.vstream, p: *u8, n: i32) (size | io.error) = { + let v: []u8; + v.ptr = p; + v.len = n; + return io.st_write(vs, v); +}; + +// vwriteone — emit one formattable through io.st_write. Mirror of +// the per-arm dispatch in fmt.fdprint (fmt.ww:103-138) and fmt.fprint +// (fmt.ww:188-234); shape matches OLD modulo the (size | io.error) +// return instead of i64 / (i32 | io.closed). +fn vwriteone(vs: io.vstream, a: formattable) (size | io.error) = { + match (a) { + case let n: i64 => { + let s: str = i64dec(n); + return vputbytes(vs, s.ptr, s.len); + }; + case let s: str => return vputbytes(vs, s.ptr, s.len); + case let b: bool => { + let s: str = "false"; + if (b) { s = "true"; }; + return vputbytes(vs, s.ptr, s.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 static-buffer view consumed before next + // strconv call — same shape as fmt.fdprint (fmt.ww:130). + let s: str = strconv.f64tos(v); + return vputbytes(vs, s.ptr, s.len); + }; + }; + 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 +// 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) = { + match (f) { + case let v: i64 => { + let a: formattable = v; + return vwriteone(vs, a); + }; + case let v: str => { + let a: formattable = v; + return vwriteone(vs, a); + }; + case let b: bool => { + let a: formattable = b; + return vwriteone(vs, a); + }; + case let r: rune => { + let a: formattable = r; + return vwriteone(vs, a); + }; + case let v: f64 => { + let a: formattable = v; + return vwriteone(vs, a); + }; + case let p: *mods => { fmtabort(); let z: size = 0; return z; }; + }; +}; + +// vfprint — vstream-side fprint. Mirror of fmt.fprint (fmt.ww:177). +// Per-arg loop with a space separator; returns total bytes written +// or the first io.error. +fn vfprint(vs: io.vstream, args: formattable...) (size | io.error) = { + let total: size = 0; + let i: i32 = 0; + for (i < args.len) { + if (i > 0) { + let r: (size | io.error) = vputbytes(vs, " ".ptr, 1); + match (r) { + case let n: size => { total += n; }; + case let e: io.error => return e; + }; + }; + let r: (size | io.error) = vwriteone(vs, args[i]); + match (r) { + case let n: size => { total += n; }; + case let e: io.error => return e; + }; + i += 1; + }; + return total; +}; + +// vfprintf — vstream-side fprintf. Mirror of fmt.fprintf (fmt.ww:676). +// {n}-placeholder parser routed through vformatfield + vputbytes +// instead of fmt.formatfield + fmt.putbytes; the placeholder language +// (incl. `{N:mods}` modifier subset) is identical — scandigits / +// scanmods / modsinit reused directly from fmt.ww. +fn vfprintf(vs: io.vstream, fmt: str, args: field...) (size | io.error) = { + let total: size = 0; + let i: i32 = 0; + let nextimpl: i32 = 0; + let checkunused: bool = true; + for (i < fmt.len) { + let c: u8 = fmt[i]; + if (c == 123u8) { // '{' + i += 1; + if (i >= fmt.len) { fmtabort(); }; + if (fmt[i] == 123u8) { // '{{' literal + let r: (size | io.error) = vputbytes(vs, fmt.ptr + i: u64, 1); + match (r) { + case let n: size => { total += n; }; + case let e: io.error => return e; + }; + i += 1; + } else { + let idx: i32 = 0; + let d: u8 = fmt[i]; + if (d >= 48u8 && d <= 57u8) { + checkunused = false; + idx = scandigits(fmt, &i); + } else { + idx = nextimpl; + nextimpl += 1; + }; + let m: mods; + modsinit(&m); + if (i < fmt.len && fmt[i] == 58u8) { // ':' + i += 1; + scanmods(fmt, &i, &m); + }; + if (i >= fmt.len || fmt[i] != 125u8) { fmtabort(); }; + i += 1; + if (idx >= args.len) { fmtabort(); }; + let r: (size | io.error) = vformatfield(vs, args[idx]); + match (r) { + case let n: size => { total += n; }; + case let e: io.error => return e; + }; + }; + } else if (c == 125u8) { // '}' + i += 1; + if (i >= fmt.len || fmt[i] != 125u8) { fmtabort(); }; + let r: (size | io.error) = vputbytes(vs, fmt.ptr + i: u64, 1); + match (r) { + case let n: size => { total += n; }; + case let e: io.error => return e; + }; + i += 1; + } else { + let r: (size | io.error) = vputbytes(vs, fmt.ptr + i: u64, 1); + match (r) { + case let n: size => { total += n; }; + case let e: io.error => return e; + }; + i += 1; + }; + }; + if (checkunused && nextimpl != args.len) { fmtabort(); }; + return total; +}; + +// ---- public fd-sink wrappers (V API) --------------------------------- + +// fdprint_v — fdprint over io.vstream. Stack-allocates fd_ctx; the +// &c.vt vstream pointer never escapes this frame (ken's escape-risk +// discipline). Mirrors fmt.fdprint (fmt.ww:94). +export fn fdprint_v(fd: i32, args: formattable...) (size | io.error) = { + let c: fd_ctx; + c.fd = fd; + c.vt.reader = (&fdsinkread_v): *io.reader; + c.vt.writer = (&fdsinkwrite_v): *io.writer; + let vs: io.vstream = &c.vt; + return vfprint(vs, args...); +}; + +// fdprintln_v — fdprint_v + trailing newline. Mirrors fmt.fdprintln +// (fmt.ww:145). +export fn fdprintln_v(fd: i32, args: formattable...) (size | io.error) = { + let c: fd_ctx; + c.fd = fd; + c.vt.reader = (&fdsinkread_v): *io.reader; + c.vt.writer = (&fdsinkwrite_v): *io.writer; + let vs: io.vstream = &c.vt; + let total: size = 0; + match (vfprint(vs, args...)) { + case let n: size => { total = n; }; + case let e: io.error => return e; + }; + match (vputbytes(vs, "\n".ptr, 1)) { + case let n: size => { total += n; }; + case let e: io.error => return e; + }; + return total; +}; + +// fdprintf_v — fdprintf over io.vstream. Mirrors fmt.fdprintf +// (fmt.ww:786). +export fn fdprintf_v(fd: i32, fmt: str, args: field...) (size | io.error) = { + let c: fd_ctx; + c.fd = fd; + c.vt.reader = (&fdsinkread_v): *io.reader; + c.vt.writer = (&fdsinkwrite_v): *io.writer; + let vs: io.vstream = &c.vt; + return vfprintf(vs, fmt, args...); +}; + +// fdprintfln_v — fdprintf_v + trailing newline. Mirrors +// fmt.fdprintfln (fmt.ww:797) and Hare's wrappers.ha:69. +export fn fdprintfln_v(fd: i32, fmt: str, args: field...) (size | io.error) = { + let c: fd_ctx; + c.fd = fd; + c.vt.reader = (&fdsinkread_v): *io.reader; + c.vt.writer = (&fdsinkwrite_v): *io.writer; + let vs: io.vstream = &c.vt; + let total: size = 0; + match (vfprintf(vs, fmt, args...)) { + case let n: size => { total = n; }; + case let e: io.error => return e; + }; + match (vputbytes(vs, "\n".ptr, 1)) { + case let n: size => { total += n; }; + case let e: io.error => return e; + }; + return total; +}; diff --git a/test/wcc/777_fmt_vstream_run.c b/test/wcc/777_fmt_vstream_run.c new file mode 100644 index 00000000..f4a2a02f --- /dev/null +++ b/test/wcc/777_fmt_vstream_run.c @@ -0,0 +1,397 @@ +/* + * 777_fmt_vstream_run — project #94 fold-e3 sentinel. Pins the + * additive lib/fmt/vstream.ww Option C parallel API: stack-resident + * fd_ctx with `vt: io.vtable` as the first field for the intrusive + * vstream cast, four wrappers (fdprint_v / fdprintln_v / fdprintf_v / + * fdprintfln_v) that take a raw fd, allocate fd_ctx on their own + * frame (never escapes — ken's dispatcher-CONTAINED discipline), and + * dispatch through io.st_write via the wired fdsinkwrite_v callback. + * Coexists with the pre-vtable fdprint / fdprintln / fdprintf / + * fdprintfln surface in fmt.ww (fold-eFinal, task #50, retires the + * latter once io fold-2 ports `handle = (file | int)`). + * + * Each row imports fmt + os + io, opens a per-process /tmp output + * file, calls one V wrapper to write through the fd, closes, re- + * opens for read, reads the bytes back, and asserts both (a) the + * exact byte content and (b) a unique row-tagged exit constant. + * + * Cstage-only per row (no STAGE_WW, no byte_id) — pre-existing + * wwstage bug #209 (sibling of #190): the wwstage checker bails + * `case: not a variant of scrutinee (X)` + `match: variant not + * handled (formattable)` on the OLD fmt.fdprint / fdprintf's + * match-on-formattable arms whenever a downstream probe `import + * fmt;`s the package. The bug bites the OLD surface identically + * — `fmt.errorln("hi")` from a probe trips the same trace. The + * pre-existing surface (fmt.ww) is fine via cstage because every + * other test that uses fmt is cstage-only (970 fmttest runs the + * @test fixture under cstage `ww run` only). 995 self-rebuild + * dodges it because no selfhost cmd imports fmt transitively — + * fmt is consumed by selfhost/cmd/wcc/err.ww but no main.ww in + * cmd/{ww,w6c,w6a,w6l,wwdump} pulls err.ww in. Fix-of-the-bug = + * one-class checker repair, out-of-scope for the additive + * fold-e3; closes the wwstage half here when it lands. Byte-id + * graduates with #209 close. + * + * row | what it pins + * --------------------------+-------------------------------------- + * fdprintf_v_int | fdprintf_v with `"v={}\n"`-style + * | format + one i64 arg. Asserts the + * | full intrusive shape (stack fd_ctx → + * | &c.vt → io.st_write → fdsinkwrite_v + * | → os.write) plus the {n}-placeholder + * | parser routed through vformatfield. + * fdprintln_v_multi | fdprintln_v with three positional + * | args (i64 + str + bool). Asserts the + * | space-separator + trailing newline + * | shape mirrors OLD fdprintln (fmt.ww: + * | 145), routed through io.st_write. + * fdprint_v_raw | fdprint_v with one str arg, no + * | format mods (zero-format raw). Pins + * | the per-arg loop without the + * | placeholder parser (vfprint code + * | path, fmt/vstream.ww). + * branched_fdprintf_v | branched callee per #105: two + * | distinct format strings runtime- + * | selected from a single fdprintf_v + * | call site. Catches a constant-fold + * | mistake in the format-string + * | dispatch (mirror of 776's + * | branched_fixed for the format-side). + * + * IMPORT-ORDER WORKAROUND (sibling task #208, pre-existing): every + * row places `import os;` FIRST for the same reason 776 documents + * (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-e3 is purely + * additive over fold-e1/e2's frozen io.* + memio.* surface): + * + * - io fold-2 (handle port): drew-deferred per fold-d/fold-e3 + * precedent. Hare's ref/hare/fmt/wrappers.ha:9-25 routes through + * io::handle; fdNNN_v collapses into bare fNNN_v once handle = + * (file | int) lands. eFinal (#50) graduates. + * + * - #206 (bare &fn → (*alias|void)): 2 cast sites per wrapper + * vtable wire-up; drop out wholesale on close. + * + * - #173 (TRY-on-tagged-return both-stages broken): fdsinkwrite_v + * constructs nomem and widens to io.error explicitly rather + * than using `os.trywrite(...)?`; same memio.vstream.ww shape. + * + * GATE POLARITY: must stay GREEN. A red here means vstream.ww + * regressed, the stack fd_ctx → vstream cast miscomputed offsets, + * the {n}-placeholder dispatch through vformatfield regressed, or + * io.vtable's first-field embed lost its offset-0 invariant. + */ +#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[] = { + { "fdprintf_v_int", + "package main;\n" + "import os;\n" + "import fmt;\n" + "import io;\n" + "export fn main() i32 = {\n" + " let path: str = \"/tmp/wwfmtv_777_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, \"v={}\\n\", 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] != 118u8 || buf[1] != 61u8 || buf[2] != 52u8 || buf[3] != 50u8 || buf[4] != 10u8) { return 95; };\n" + " return 42;\n" + "};\n", + 42, + STAGE_CS, 0 }, + { "fdprintln_v_multi", + "package main;\n" + "import os;\n" + "import fmt;\n" + "import io;\n" + "export fn main() i32 = {\n" + " let path: str = \"/tmp/wwfmtv_777_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.fdprintln_v(fd, 7i64, \"hi\", true);\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 != 10i64) { return 93; };\n" + " if (nw != 10: size) { return 94; };\n" + " if (buf[0] != 55u8 || buf[1] != 32u8 || buf[2] != 104u8 || buf[3] != 105u8 || buf[4] != 32u8 || buf[5] != 116u8 || buf[6] != 114u8 || buf[7] != 117u8 || buf[8] != 101u8 || buf[9] != 10u8) { return 95; };\n" + " return 43;\n" + "};\n", + 43, + STAGE_CS, 0 }, + { "fdprint_v_raw", + "package main;\n" + "import os;\n" + "import fmt;\n" + "import io;\n" + "export fn main() i32 = {\n" + " let path: str = \"/tmp/wwfmtv_777_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.fdprint_v(fd, \"raw\");\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: [8]u8;\n" + " let n: i64 = os.read(rd, &buf[0], 8u64);\n" + " os.close(rd);\n" + " if (n != 3i64) { return 93; };\n" + " if (nw != 3: size) { return 94; };\n" + " if (buf[0] != 114u8 || buf[1] != 97u8 || buf[2] != 119u8) { return 95; };\n" + " return 44;\n" + "};\n", + 44, + STAGE_CS, 0 }, + { "branched_fdprintf_v", + "package main;\n" + "import os;\n" + "import fmt;\n" + "import io;\n" + "export fn main() i32 = {\n" + " let path: str = \"/tmp/wwfmtv_777_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 sel: i32 = 1;\n" + " let f1: str = \"A{}\";\n" + " let f2: str = \"B{}\";\n" + " let fmtstr: str = f1;\n" + " if (sel == 0) { fmtstr = f2; };\n" + " let wr = fmt.fdprintf_v(fd, fmtstr, 9i64);\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: [8]u8;\n" + " let n: i64 = os.read(rd, &buf[0], 8u64);\n" + " os.close(rd);\n" + " if (n != 2i64) { return 93; };\n" + " if (nw != 2: size) { return 94; };\n" + " if (buf[0] != 65u8 || buf[1] != 57u8) { return 95; };\n" + " return 45;\n" + "};\n", + 45, + 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. Sweep all then + * rmdir. Mirror of 776's cleanup_tmp. */ +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/fvs_%d_d_%d", getpid(), seq); + snprintf(base, sizeof base, "main777"); + 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 — diff cstage vs wwstage .s. Parallel trees so + * ww_ww writing intermediates next to the source doesn't clobber the + * cstage .s (CLAUDE.md rule 14 phase split). Mirror of 776's. */ +static int +asm_byte_identical(const char *cdrv, const char *wdrv, const char *cwd, + const struct row *r, int seq) +{ + char src[512], tdc[256], tdw[256], base[64], cs[512], ws[512]; + snprintf(tdc, sizeof tdc, "/tmp/fvs_%d_c_%d", getpid(), seq); + snprintf(tdw, sizeof tdw, "/tmp/fvs_%d_w_%d", getpid(), seq); + snprintf(base, sizeof base, "main777"); + mkdir(tdc, 0755); + mkdir(tdw, 0755); + snprintf(src, sizeof src, "%s/%s.ww", tdc, base); + if (write_source(src, r->src) != 0) { cleanup_tmp(tdc, base); cleanup_tmp(tdw, base); return -1; } + int rc = -1; + if (build_via_driver(cdrv, tdc, cwd, src) != 0) goto out; + snprintf(cs, sizeof cs, "%s/%s.s", tdc, base); + + snprintf(src, sizeof src, "%s/%s.ww", tdw, base); + if (write_source(src, r->src) != 0) goto out; + if (build_via_driver(wdrv, tdw, cwd, src) != 0) goto out; + snprintf(ws, sizeof ws, "%s/%s.s", tdw, base); + + FILE *fc = fopen(cs, "rb"); + FILE *fw = fopen(ws, "rb"); + if (fc && fw) { + rc = 0; + 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); +out: + cleanup_tmp(tdc, base); + cleanup_tmp(tdw, 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_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_run[ww][%s]: exit=%d want=%d\n", + rows[i].label, got, rows[i].want_exit); + fail++; + } + if (rows[i].byte_id) { + total++; + if (asm_byte_identical(cdrv, wdrv, cwd, &rows[i], seq++) != 0) { + fprintf(stderr, + "fmt_vstream_run[byte-id][%s]: cstage vs wwstage asm differs\n", + rows[i].label); + fail++; + } + } + } + } + + if (!wwpresent) + fprintf(stderr, "fmt_vstream_run: skip wwstage (no %s)\n", wdrv); + + if (fail) { + fprintf(stderr, "fmt_vstream_run: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("fmt_vstream_run: %d/%d ok\n", total, total); + return 0; +}