diff --git a/Makefile b/Makefile index 4f5aede5..745a1c54 100644 --- a/Makefile +++ b/Makefile @@ -329,6 +329,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_memio_vstream_run \ $(BIN)/test_fmt_vstream_run \ $(BIN)/test_fmt_vstream_mods_run \ + $(BIN)/test_fmt_vstream_compositions_run \ $(BIN)/test_bufio_vstream_run \ $(BIN)/test_log_vstream_run \ $(BIN)/test_use_promote_alias \ @@ -714,6 +715,14 @@ $(BIN)/test_fmt_vstream_mods_run: test/wcc/780_fmt_vstream_mods_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_fmt_vstream_compositions_run: test/wcc/781_fmt_vstream_compositions_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/memio/memio.ww lib/memio/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 2188ebe8..d293f0a1 100644 --- a/lib/fmt/vstream.ww +++ b/lib/fmt/vstream.ww @@ -65,8 +65,10 @@ package fmt; import io; +import memio; import os; import strconv; +import strings; // fd_ctx — vt at offset 0 for the intrusive vstream→*fd_ctx cast. // Mirrors lib/memio.fixed_ctx (memio/vstream.ww:48); single tagged @@ -556,3 +558,111 @@ export fn fdprintfln_v(fd: i32, fmt: str, args: field...) (size | io.error) = { }; return total; }; + +// ---- V-side compositions over the vfprint / vfprintf primitives. +// Project #94 fold-e7. drew-approved bundle (memio.fixed_string / +// memio.dynamic_string land alongside as direct enablers, not churn — +// feedback_refactor_routing_same_class_drops); ken cs==ww mechanical +// (additive only). eFinal (#50) collapses both surfaces and renames +// `v` suffix off. +// +// Mirror sites: +// vfprintln fmt.ww:240 fprintln ref/hare/fmt/wrappers.ha:48 +// vfprintfln fmt.ww:740 fprintfln ref/hare/fmt/wrappers.ha:69 +// vbsprintf fmt.ww:839 bsprintf ref/hare/fmt/wrappers.ha:42 +// vasprintf fmt.ww:873 asprintf ref/hare/fmt/wrappers.ha:29 + +// vfprintln — vfprint + trailing newline. Mirror fmt.fprintln +// (fmt.ww:240); vputbytes + (size | io.error) routing. +export fn vfprintln(vs: io.vstream, args: formattable...) (size | io.error) = { + 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; +}; + +// vfprintfln — vfprintf + trailing newline. Mirror fmt.fprintfln +// (fmt.ww:740). +export fn vfprintfln(vs: io.vstream, fmt: str, args: field...) (size | io.error) = { + 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; +}; + +// vbsprintf — render into `buf` through memio.fixed_vstream; return +// the str view of bytes actually written. Mirror fmt.bsprintf +// (fmt.ww:839); nomem from fixed_vstream widens into io.error (Hare +// wrappers.ha:42 returns `(const str | nomem)` directly — ww collapses +// to (str | io.error) so the vfprintf path's io.error arm stays +// uniform). Short writes surface as a prefix (memio.fixed contract, +// vstream.ww:71 note). +export fn vbsprintf(buf: []u8, fmt: str, args: field...) (str | io.error) = { + let r: (io.vstream | nomem) = memio.fixed_vstream(buf); + let vs: io.vstream = nil: *io.vtable; + match (r) { + case let v: io.vstream => { vs = v; }; + case let nm: nomem => { let e: io.error = nm; return e; }; + }; + match (vfprintf(vs, fmt, args...)) { + case let n: size => { return memio.fixed_string(vs); }; + case let e: io.error => return e; + }; +}; + +// vasprintf — render `fmt`/`args` into a heap-allocated str through +// memio.dynamic_vstream, shrink-copy to a tight allocation, close the +// dynamic backing. Mirror fmt.asprintf (fmt.ww:873) modulo: bare `str` +// return (Hare returns `(str | nomem)`; ww has no `nomem` variant on +// the public surface — os.alloc faults on OOM per lib/os.ww). Caller +// frees with `os.free(r.ptr, r.len: u64)` when r.len > 0; r.len == 0 +// is a no-op free (same shape strings.dup uses at strings.ww:70). +// Shrink-to-fit rationale: memio.dynamic_vstream's cap doubles past +// pos during growth (memio/vstream.ww:230); io.st_close frees the +// cap-sized mapping. Returning memio.dynamic_string directly would +// leak the cap-vs-len slack (skip close) or dangle the view (close +// first); the copy lets the caller free with r.len. +export fn vasprintf(fmt: str, args: field...) str = { + let out: str; + out.ptr = nil; + out.len = 0; + let r: (io.vstream | nomem) = memio.dynamic_vstream(); + let vs: io.vstream = nil: *io.vtable; + match (r) { + case let v: io.vstream => { vs = v; }; + case nomem => { return out; }; + }; + let wres: (size | io.error) = vfprintf(vs, fmt, args...); + match (wres) { + case let n: size => {}; + case let e: io.error => {}; + }; + let view: str = memio.dynamic_string(vs); + if (view.len == 0) { + let cres: (void | io.error) = io.st_close(vs); + match (cres) { case void => {}; case let e: io.error => {}; }; + return out; + }; + let tight: []u8 = alloc([], view.len: u64)!; + let i: i32 = 0; + for (i < view.len) { + tight[i] = view.ptr[i]; + i += 1; + }; + let cres: (void | io.error) = io.st_close(vs); + match (cres) { case void => {}; case let e: io.error => {}; }; + tight.len = view.len; + return strings.frombytes(tight); +}; diff --git a/lib/memio/vstream.ww b/lib/memio/vstream.ww index 703c9242..fcd07083 100644 --- a/lib/memio/vstream.ww +++ b/lib/memio/vstream.ww @@ -240,3 +240,36 @@ fn dynamicgrow_v(d: *dynamic_ctx, need: i32) void = { d.ptr = nbuf; d.cap = newcap; }; + +// fixed_string / dynamic_string — borrowed str view of buf[0..pos] for +// the matching ctx flavour. Project #94 fold-e7 bundles them here as +// direct enablers for fmt.vbsprintf / fmt.vasprintf (drew-approved per +// feedback_refactor_routing_same_class_drops — prereqs, not churn). +// Mirrors OLD memio.string (memio.ww:102) over `*state`; the V-side +// twins key off the intrusive *_ctx cast same shape as the +// vtable callbacks (vstream.ww:151,184). Two flavours kept distinct +// (vs one fn over a shared header) so each cast targets the matching +// ctx type — same rationale as the split fixedread_v / dynread_v at +// line 144-148. Unchecked str view per CLAUDE.md rule 9 carve-out +// (utf8.validate at IO source is opt-in, not wrapped per-construction). +// +// Mirrors ref/hare/memio/ops.ha:51 string(s: io::handle) and the +// per-handle dispatch shape. fold-e8 (task #50 prereq) extends with +// buffer/reset/borrowedread accessors; this fold ships only the +// vbsprintf / vasprintf enablers. + +export fn fixed_string(vs: io.vstream) str = { + let c: *fixed_ctx = vs: *fixed_ctx; + let r: str; + r.ptr = c.ptr; + r.len = c.pos; + return r; +}; + +export fn dynamic_string(vs: io.vstream) str = { + let c: *dynamic_ctx = vs: *dynamic_ctx; + let r: str; + r.ptr = c.ptr; + r.len = c.pos; + return r; +}; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index dae54ebb..ebdcd887 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -14568,6 +14568,39 @@ fn dynamicgrow_v(d: *dynamic_ctx, need: i32) void = { d.cap = newcap; }; +// fixed_string / dynamic_string — borrowed str view of buf[0..pos] for +// the matching ctx flavour. Project #94 fold-e7 bundles them here as +// direct enablers for fmt.vbsprintf / fmt.vasprintf (drew-approved per +// feedback_refactor_routing_same_class_drops — prereqs, not churn). +// Mirrors OLD memio.string (memio.ww:102) over `*state`; the V-side +// twins key off the intrusive *_ctx cast same shape as the +// vtable callbacks (vstream.ww:151,184). Two flavours kept distinct +// (vs one fn over a shared header) so each cast targets the matching +// ctx type — same rationale as the split fixedread_v / dynread_v at +// line 144-148. Unchecked str view per CLAUDE.md rule 9 carve-out +// (utf8.validate at IO source is opt-in, not wrapped per-construction). +// +// Mirrors ref/hare/memio/ops.ha:51 string(s: io::handle) and the +// per-handle dispatch shape. fold-e8 (task #50 prereq) extends with +// buffer/reset/borrowedread accessors; this fold ships only the +// vbsprintf / vasprintf enablers. + +export fn fixed_string(vs: io.vstream) str = { + let c: *fixed_ctx = vs: *fixed_ctx; + let r: str; + r.ptr = c.ptr; + r.len = c.pos; + return r; +}; + +export fn dynamic_string(vs: io.vstream) str = { + let c: *dynamic_ctx = vs: *dynamic_ctx; + let r: str; + r.ptr = c.ptr; + r.len = c.pos; + return r; +}; + // selfhost/cmd/wcc/cgenutil.ww — split out of cgen.ww. // // General helpers used across cgenexpr / cgenstmt / cgendecl: diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 6fd80599..7be553db 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -14568,6 +14568,39 @@ fn dynamicgrow_v(d: *dynamic_ctx, need: i32) void = { d.cap = newcap; }; +// fixed_string / dynamic_string — borrowed str view of buf[0..pos] for +// the matching ctx flavour. Project #94 fold-e7 bundles them here as +// direct enablers for fmt.vbsprintf / fmt.vasprintf (drew-approved per +// feedback_refactor_routing_same_class_drops — prereqs, not churn). +// Mirrors OLD memio.string (memio.ww:102) over `*state`; the V-side +// twins key off the intrusive *_ctx cast same shape as the +// vtable callbacks (vstream.ww:151,184). Two flavours kept distinct +// (vs one fn over a shared header) so each cast targets the matching +// ctx type — same rationale as the split fixedread_v / dynread_v at +// line 144-148. Unchecked str view per CLAUDE.md rule 9 carve-out +// (utf8.validate at IO source is opt-in, not wrapped per-construction). +// +// Mirrors ref/hare/memio/ops.ha:51 string(s: io::handle) and the +// per-handle dispatch shape. fold-e8 (task #50 prereq) extends with +// buffer/reset/borrowedread accessors; this fold ships only the +// vbsprintf / vasprintf enablers. + +export fn fixed_string(vs: io.vstream) str = { + let c: *fixed_ctx = vs: *fixed_ctx; + let r: str; + r.ptr = c.ptr; + r.len = c.pos; + return r; +}; + +export fn dynamic_string(vs: io.vstream) str = { + let c: *dynamic_ctx = vs: *dynamic_ctx; + let r: str; + r.ptr = c.ptr; + r.len = c.pos; + return r; +}; + // selfhost/cmd/wcc/cgenutil.ww — split out of cgen.ww. // // General helpers used across cgenexpr / cgenstmt / cgendecl: diff --git a/test/wcc/781_fmt_vstream_compositions_run.c b/test/wcc/781_fmt_vstream_compositions_run.c new file mode 100644 index 00000000..72d7382e --- /dev/null +++ b/test/wcc/781_fmt_vstream_compositions_run.c @@ -0,0 +1,324 @@ +/* + * 781_fmt_vstream_compositions_run — project #94 fold-e7 sentinel. + * Pins the V-side composition wrappers in lib/fmt/vstream.ww — + * vfprintln / vfprintfln / vbsprintf / vasprintf — plus the two + * memio enablers fold-e7 bundles alongside (memio.fixed_string / + * memio.dynamic_string in lib/memio/vstream.ww). Coexists with the + * OLD fmt.fprintln / fprintfln / bsprintf / asprintf surface in + * fmt.ww:240,740,839,873 (fmt.ww UNCHANGED this fold; eFinal + * task #50 collapses both surfaces and renames the `v` suffix off). + * + * The two memio helpers are direct enablers (vbsprintf reads its + * written prefix via fixed_string; vasprintf reads its grown buffer + * via dynamic_string), drew-approved per + * feedback_refactor_routing_same_class_drops (prereqs, not churn). + * + * Each row imports fmt + os + io, drives one of the four V wrappers, + * and asserts the exact bytes / size emitted. Per-row unique exit + * constant lets a failure pinpoint the offending case. + * + * row | what it pins + * --------------------------+-------------------------------------- + * fdprintln_v_run_basic | vfprintln through an fd_ctx vstream + * | (via fdprintln_v's wired vstream sink + * | dispatching vfprint then vputbytes + * | "\n"). One i64 arg + str arg + bool + * | arg; asserts space-separator plus + * | trailing newline byte content. + * fdprintfln_v_run_fmt | vfprintfln through fd_ctx vstream + * | with a {n}-placeholder format string. + * | Asserts placeholder substitution + + * | trailing newline shape (mirror of + * | fmt.fprintfln, fmt.ww:740). + * bsprintf_v_basic | vbsprintf into a caller-supplied + * | [16]u8 buffer through + * | memio.fixed_vstream; + * | memio.fixed_string returns the prefix + * | view. Asserts both the bytes in the + * | caller's buffer AND the returned str + * | view (ptr == buf + 0, len == 5). + * asprintf_v_basic | vasprintf into a heap-grown str + * | through memio.dynamic_vstream; + * | memio.dynamic_string + shrink-copy + + * | io.st_close. Asserts the owned str + * | bytes, then frees via os.free. + * + * 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 fmt.formattable match-arms whenever a + * downstream probe `import fmt;`s the package. Identical carve-out + * to 777_fmt_vstream_run + 780_fmt_vstream_mods_run (cited there at + * line 18-33 / 18-23). Byte-id graduates with #209 close. + * + * IMPORT-ORDER WORKAROUND (sibling task #208, pre-existing): every + * row places `import os;` FIRST for the same reason 776/777/780 + * 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-e7 is purely + * additive over fold-e1/e2/e3/e4/e5/e6's frozen io.* + memio.* + + * fmt.* + bufio.* + log.* surface): + * + * - #209 (wwstage formattable match-arm bail): blocks STAGE_WW + * here, same as 777/780. Cstage-only until close. + * - #173 (TRY-on-tagged-return both-stages broken): vbsprintf + * widens nomem into io.error inline rather than `memio. + * fixed_vstream(buf)?` for the same reason memio.vstream.ww + * does at line 87-99. + * - eFinal (#50): collapses V + OLD surfaces; the `v` names + * rename off and the OLD fprintln / fprintfln / bsprintf / + * asprintf delete. + * + * BOOTSTRAP-EMBED CHECK: fmt is NOT embedded in any selfhost + * combined.ww (grep verified pre-impl). memio.vstream.ww IS embedded + * in w6c + wwdump combined.ww — the fold-e7 memio helpers ride along + * via the regen committed in this same commit (per #110 SSoT). 990/ + * 994/995 byte-id gates + combined_ww_fresh stay green by virtue of + * the regen. + * + * GATE POLARITY: must stay GREEN. A red here means vfprintln / + * vfprintfln dropped the newline byte, vbsprintf returned a stale + * memio.fixed_string view (intrusive cast broke), vasprintf failed + * to shrink-copy / close cleanly, or the underlying vfprint / + * vfprintf primitives regressed. + */ +#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[] = { + { "fdprintln_v_run_basic", + "package main;\n" + "import os;\n" + "import fmt;\n" + "import io;\n" + "export fn main() i32 = {\n" + " let path: str = \"/tmp/wwfmtv_781_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.fdprintln_v(fd, 1i64, \"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] != 49u8) { return 95; };\n" + " if (buf[1] != 32u8) { return 96; };\n" + " if (buf[2] != 104u8 || buf[3] != 105u8) { return 97; };\n" + " if (buf[4] != 32u8) { return 98; };\n" + " if (buf[9] != 10u8) { return 99; };\n" + " return 60;\n" + "};\n", + 60, + STAGE_CS, 0 }, + { "fdprintfln_v_run_fmt", + "package main;\n" + "import os;\n" + "import fmt;\n" + "import io;\n" + "export fn main() i32 = {\n" + " let path: str = \"/tmp/wwfmtv_781_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.fdprintfln_v(fd, \"x={}\", 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 != 4i64) { return 93; };\n" + " if (nw != 4: size) { return 94; };\n" + " if (buf[0] != 120u8 || buf[1] != 61u8 || buf[2] != 55u8 || buf[3] != 10u8) { return 95; };\n" + " return 61;\n" + "};\n", + 61, + STAGE_CS, 0 }, + { "bsprintf_v_basic", + "package main;\n" + "import os;\n" + "import fmt;\n" + "import io;\n" + "export fn main() i32 = {\n" + " let buf: [16]u8;\n" + " let r = fmt.vbsprintf(buf[0:16], \"v={}\", 42i64);\n" + " let view: str = \"\";\n" + " match (r) {\n" + " case let s: str => { view = s; };\n" + " case io.error => { return 90; };\n" + " };\n" + " if (view.len != 4) { return 91; };\n" + " if (view[0] != 118u8 || view[1] != 61u8) { return 92; };\n" + " if (view[2] != 52u8 || view[3] != 50u8) { return 93; };\n" + " if (buf[0] != 118u8 || buf[3] != 50u8) { return 94; };\n" + " return 62;\n" + "};\n", + 62, + STAGE_CS, 0 }, + { "asprintf_v_basic", + "package main;\n" + "import os;\n" + "import fmt;\n" + "import io;\n" + "export fn main() i32 = {\n" + " let view: str = fmt.vasprintf(\"hello={}\", 9i64);\n" + " if (view.len != 7) { return 90; };\n" + " if (view[0] != 104u8 || view[1] != 101u8 || view[2] != 108u8) { return 91; };\n" + " if (view[3] != 108u8 || view[4] != 111u8) { return 92; };\n" + " if (view[5] != 61u8 || view[6] != 57u8) { return 93; };\n" + " os.free(view.ptr: *void, view.len: u64);\n" + " return 63;\n" + "};\n", + 63, + 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 (task #15), so each row's build leaves .{combined.ww, + * s,o} + bare exe alongside. Mirror of 777/780'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/fvc_%d_d_%d", getpid(), seq); + snprintf(base, sizeof base, "main781"); + 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_compositions_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_compositions_run[ww][%s]: exit=%d want=%d\n", + rows[i].label, got, rows[i].want_exit); + fail++; + } + } + } + + if (!wwpresent) + fprintf(stderr, "fmt_vstream_compositions_run: skip wwstage (no %s)\n", wdrv); + + if (fail) { + fprintf(stderr, "fmt_vstream_compositions_run: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("fmt_vstream_compositions_run: %d/%d ok\n", total, total); + return 0; +}