From b842f9337b344673c36e80a993ee3ded17148692 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 29 May 2026 08:42:36 +0900 Subject: [PATCH] lib/memio: add Option C parallel vstream API (#94 fold-e2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds lib/memio/vstream.ww with three new constructors — fixed_vstream / dynamic_vstream / dynamicfrom_vstream — that return io.vstream (= *io.vtable, from lib/io/stream.ww) alongside the pre-vtable memio.fixed / dynamic / dynamicfrom shape in memio.ww. Hare's memio::fixed/dynamic return a `stream` whose first field IS io::stream (= *vtable); ww mirrors that intrusively with fixed_ctx + dynamic_ctx structs whose first field is `vt: io.vtable`. A heap-alloc'd *fixed_ctx is castable to vstream via `&c.vt`, and callbacks recover the outer ctx via `s: *fixed_ctx` (same pattern as lib/bufio.stream over io.stream and lib/log.stdlogger over logger). ptr/len/cap kept flat (memio.ww:39 SOP) to dodge the chained-dot-through-pointer-into-slice-subfield miscompile family. Constructor flow: alloc with vt zero-initialised via a local, then chained `c.vt.X = …` field-assigns through the *ctx pointer. Struct-lit init via `vt = local_vt` (with local pre-set) silently drops tagged-union slots past the first — sibling task filed, workaround is the alloc-then-assign route (proven byte-id between both stages). *ctx is a plain pointer-to-struct, not an aliased pointer, so the chained-store path doesn't hit #195. Cast workaround per #206 at each vtable-fn-ptr-slot init (8 sites across the 3 constructors): bare `&fn_name` does not type-check as `(* | void)`. Same `(&fn): *io.` shape that test/wcc/775_io_vtable_run.c uses. Drops out when #206 closes. OLD memio surface is UNCHANGED. fold-eFinal (task #50) atomically flips the package shape: deletes OLD constructors + callbacks and renames `_vstream` suffix off. Probe test/wcc/776_memio_vstream_run.c: 4 rows (fixed_read_5 / dynamic_write_grow / dynamicfrom_alt_rw / branched_fixed) exercise both vtable flavours through the io.st_read / st_write / st_close dispatchers. Each row drives cs runtime + ww runtime + cs.s == ww.s byte-id — 12 fixtures total, all green. Pre-existing wwstage gap surfaced + documented inline: ww_ww's combined.ww concat order trips the wwstage checker on os.tryread / trywrite / tryopen's bare `return r;` over `(int | oserror)` when os is checked after rt/io. Each probe row places `import os;` FIRST to match the ordering selfhost uses (time → os → rt → …) where the checker resolves cleanly. Sibling task; resolves the ordering-sensitivity in the wwstage checker drops the workaround. --- Makefile | 12 +- lib/memio/vstream.ww | 242 ++++++++++++++++ selfhost/cmd/w6c/main.combined.ww | 243 ++++++++++++++++ selfhost/cmd/wwdump/main.combined.ww | 243 ++++++++++++++++ test/wcc/776_memio_vstream_run.c | 398 +++++++++++++++++++++++++++ 5 files changed, 1136 insertions(+), 2 deletions(-) create mode 100644 lib/memio/vstream.ww create mode 100644 test/wcc/776_memio_vstream_run.c diff --git a/Makefile b/Makefile index 6ef03122..a813e8ff 100644 --- a/Makefile +++ b/Makefile @@ -113,7 +113,7 @@ $(BIN)/wwdump_ww: selfhost/cmd/wwdump/main.ww \ selfhost/cmd/wcc/cgen.ww selfhost/cmd/wcc/cgenexpr.ww \ selfhost/cmd/wcc/cgenstmt.ww selfhost/cmd/wcc/cgenutil.ww \ selfhost/cmd/wcc/cgendecl.ww \ - lib/os/os.ww lib/rt/malloc.ww lib/time/time.ww lib/strconv/strconv.ww lib/strconv/stof_data.ww lib/strconv/decimal.ww lib/strconv/stof.ww lib/strconv/ftos_data.ww lib/strconv/ftos.ww lib/strings/strings.ww lib/bytes/bytes.ww lib/encoding/utf8/utf8.ww lib/ascii/ascii.ww lib/errors/errors.ww lib/io/io.ww lib/io/stream.ww lib/io/types.ww lib/memio/memio.ww lib/fmt/fmt.ww \ + lib/os/os.ww lib/rt/malloc.ww lib/time/time.ww lib/strconv/strconv.ww lib/strconv/stof_data.ww lib/strconv/decimal.ww lib/strconv/stof.ww lib/strconv/ftos_data.ww lib/strconv/ftos.ww lib/strings/strings.ww lib/bytes/bytes.ww lib/encoding/utf8/utf8.ww lib/ascii/ascii.ww lib/errors/errors.ww lib/io/io.ww lib/io/stream.ww lib/io/types.ww lib/memio/memio.ww lib/memio/vstream.ww lib/fmt/fmt.ww \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) @mkdir -p $(BIN)/wwdump_ww.d @@ -135,7 +135,7 @@ $(BIN)/w6c_ww: selfhost/cmd/w6c/main.ww \ selfhost/cmd/wcc/cgen.ww selfhost/cmd/wcc/cgenexpr.ww \ selfhost/cmd/wcc/cgenstmt.ww selfhost/cmd/wcc/cgenutil.ww \ selfhost/cmd/wcc/cgendecl.ww \ - lib/os/os.ww lib/rt/malloc.ww lib/time/time.ww lib/strconv/strconv.ww lib/strconv/stof_data.ww lib/strconv/decimal.ww lib/strconv/stof.ww lib/strconv/ftos_data.ww lib/strconv/ftos.ww lib/strings/strings.ww lib/bytes/bytes.ww lib/encoding/utf8/utf8.ww lib/ascii/ascii.ww lib/errors/errors.ww lib/io/io.ww lib/io/stream.ww lib/io/types.ww lib/memio/memio.ww lib/fmt/fmt.ww \ + lib/os/os.ww lib/rt/malloc.ww lib/time/time.ww lib/strconv/strconv.ww lib/strconv/stof_data.ww lib/strconv/decimal.ww lib/strconv/stof.ww lib/strconv/ftos_data.ww lib/strconv/ftos.ww lib/strings/strings.ww lib/bytes/bytes.ww lib/encoding/utf8/utf8.ww lib/ascii/ascii.ww lib/errors/errors.ww lib/io/io.ww lib/io/stream.ww lib/io/types.ww lib/memio/memio.ww lib/memio/vstream.ww lib/fmt/fmt.ww \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) @mkdir -p $(BIN)/w6c_ww.d @@ -326,6 +326,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_isas_spread_variant \ $(BIN)/test_tagged_widen_named_variant \ $(BIN)/test_io_vtable_run \ + $(BIN)/test_memio_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 \ @@ -688,6 +689,13 @@ $(BIN)/test_io_vtable_run: test/wcc/775_io_vtable_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_memio_vstream_run: test/wcc/776_memio_vstream_run.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + lib/memio/memio.ww lib/memio/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/memio/vstream.ww b/lib/memio/vstream.ww new file mode 100644 index 00000000..703c9242 --- /dev/null +++ b/lib/memio/vstream.ww @@ -0,0 +1,242 @@ +// vstream — Hare-shaped vtable wrappers over memio. Project #94 +// fold-e2 (Option C, parallel API). +// +// Adds three constructors that return `io.vstream` (= `*io.vtable`, +// the Hare-shape from lib/io/stream.ww) alongside the pre-vtable +// memio.fixed / memio.dynamic / memio.dynamicfrom in memio.ww. The +// OLD surface stays untouched here — fold-eFinal (task #50) atomically +// flips the package shape: deletes the OLD constructors + callbacks, +// renames `_vstream` suffix off, and migrates the few callers. +// +// Hare's memio::fixed/dynamic return `stream` whose FIRST field IS +// `io::stream` (= `*vtable`). ww mirrors that intrusively: each ctx +// struct's first field is `vt: io.vtable` (the vtable embedded +// INLINE, not a pointer to it) so a heap-alloc'd `*fixed_ctx` is +// castable to `vstream = *vtable` via `&c.vt` — and the callbacks +// recover the outer ctx by casting the dispatch arg back to +// `*fixed_ctx`. Same intrusive shape as lib/bufio.stream over +// io.stream (bufio.ww:240-261) and lib/log.stdlogger over logger +// (log.ww:95-98). +// +// Cast workaround per #206: bare `&fn_name` does not type-check as +// a `(* | void)` field-init / let-binding (the structural +// `*fn(...)` value isn't accepted as the `*reader` named variant +// of the tagged slot). Explicit `(&fn_name): *io.` cast at +// each store site is the Hare-faithful minimum-touch route — same +// workaround test/wcc/775_io_vtable_run.c uses for the bare-vtable +// init. 8 cast tokens here (2 in fixed_vstream + 3 each in +// dynamic_vstream / dynamicfrom_vstream — fixed leaves closer +// void per Hare's fixed_vt, which lets st_close's void-arm return +// plain `void` with no callback needed). Casts drop out wholesale +// once #206 closes. +// +// ptr/len/cap kept flat (no `buf: []u8`) per the memio.state note +// (memio.ww:39): chained-dot writes through a state pointer into a +// slice subfield miscompile silently (related to the #195 family); +// the flat shape sidesteps it. dynamicfrom_vstream uses the slice's +// `cap` (NOT `len`) to track the allocated-capacity-to-free on +// close — mirrors the same OLD memio.dynamicfrom note (memio.ww:73). + +package memio; + +import io; +import os; +import rt; + +// fixed_ctx — heap-alloc'd state for fixed_vstream. `vt` at offset 0 +// for the intrusive vstream cast. +export type fixed_ctx = struct { + vt: io.vtable, + ptr: *u8, + len: i32, + cap: i32, + pos: i32, +}; + +// dynamic_ctx — heap-alloc'd state for dynamic_vstream / +// dynamicfrom_vstream. Same intrusive shape as fixed_ctx; the +// difference is the vtable wired (writer=dynamicwrite_v, +// closer=dynamicclose_v) and that the buffer can grow via +// dynamicgrow_v on write overflow. +export type dynamic_ctx = struct { + vt: io.vtable, + ptr: *u8, + len: i32, + cap: i32, + pos: i32, +}; + +// fixed_vstream — wire a vstream over a caller-supplied buffer. +// Writes never grow; they return 0 once `pos` reaches the end of +// the buffer (Hare returns `nomem` here; ww surfaces 0 to mirror +// the OLD memio.fixedwrite divergence — graduating to Hare's +// `nomem` return needs the widen-from-bare-nomem path that #173 +// blocks, deferred to eFinal). +// +// Mirrors ref/hare/memio/stream.ha:47. +// +// Three steps (vs Hare's single struct literal): alloc with vt +// zero-init (struct-lit `vt = local_vt` silently drops tagged-union +// fields past the first — sibling task; the `let zero` + post-alloc +// chained `c.vt.X = …` field-assigns through the *fixed_ctx pointer +// is the proven-working route, same shape lib/bufio.init uses on +// b.vtable). Field-assign of a TY_PTR/void variant through the +// chained pointer-into-struct is fine here (*fixed_ctx is a plain +// pointer-to-struct, not an aliased pointer; #195's "aliased-ptr +// receiver" carve-out doesn't bite). +export fn fixed_vstream(buf: []u8) (io.vstream | nomem) = { + let zero: io.vtable; + let c: *fixed_ctx = alloc(fixed_ctx{ + vt = zero, + ptr = buf.ptr, + len = buf.len, + cap = buf.len, + pos = 0, + })?; + c.vt.reader = (&fixedread_v): *io.reader; + c.vt.writer = (&fixedwrite_v): *io.writer; + return &c.vt; +}; + +// dynamic_vstream — wire a vstream with no initial buffer. Writes +// grow the backing allocation; [[io.st_close]] frees it. +// +// Mirrors ref/hare/memio/stream.ha:54. +export fn dynamic_vstream() (io.vstream | nomem) = { + let zero: io.vtable; + let c: *dynamic_ctx = alloc(dynamic_ctx{ + vt = zero, + ptr = nil, + len = 0, + cap = 0, + pos = 0, + })?; + c.vt.reader = (&dynread_v): *io.reader; + c.vt.writer = (&dynamicwrite_v): *io.writer; + c.vt.closer = (&dynamicclose_v): *io.closer; + return &c.vt; +}; + +// dynamicfrom_vstream — like [[dynamic_vstream]] but seeded with an +// existing slice. Ownership transfers; close frees `cap` bytes from +// the slice's allocated capacity (NOT logical length) — passing a +// half-filled append slice with len < cap and using only `buf.len` +// would under-free on close. +// +// Mirrors ref/hare/memio/stream.ha:65. +export fn dynamicfrom_vstream(buf: []u8) (io.vstream | nomem) = { + let zero: io.vtable; + let c: *dynamic_ctx = alloc(dynamic_ctx{ + vt = zero, + ptr = buf.ptr, + len = buf.len, + cap = buf.cap, + pos = 0, + })?; + c.vt.reader = (&dynread_v): *io.reader; + c.vt.writer = (&dynamicwrite_v): *io.writer; + c.vt.closer = (&dynamicclose_v): *io.closer; + return &c.vt; +}; + +// ---- vtable callbacks ---------------------------------------------------- + +// fixedread_v / dynread_v — recover the ctx from vstream's +// `*vtable` via the intrusive offset-0 cast. Separate fns (vs +// sharing one readfn) so each cast targets the matching ctx type +// — the Hare-side `s: *stream` cast in ref/hare/memio/stream.ha:103 +// has the same shape but only one ctx flavour. + +fn fixedread_v(s: io.vstream, buf: []u8) (size | io.eof | io.error) = { + let m: *fixed_ctx = s: *fixed_ctx; + if (m.pos >= m.len) { + let e: io.eof; + return e; + }; + let avail: i32 = m.len - m.pos; + let n: i32 = buf.len; + if (avail < n) { n = avail; }; + let i: i32 = 0; + for (i < n) { + buf[i] = m.ptr[m.pos + i]; + i += 1; + }; + m.pos += n; + return n: size; +}; + +fn fixedwrite_v(s: io.vstream, buf: []u8) (size | io.error) = { + let m: *fixed_ctx = s: *fixed_ctx; + if (m.pos >= m.len) { return 0: size; }; + let space: i32 = m.len - m.pos; + let n: i32 = buf.len; + if (space < n) { n = space; }; + let i: i32 = 0; + for (i < n) { + m.ptr[m.pos + i] = buf[i]; + i += 1; + }; + m.pos += n; + return n: size; +}; + +fn dynread_v(s: io.vstream, buf: []u8) (size | io.eof | io.error) = { + let m: *dynamic_ctx = s: *dynamic_ctx; + if (m.pos >= m.len) { + let e: io.eof; + return e; + }; + let avail: i32 = m.len - m.pos; + let n: i32 = buf.len; + if (avail < n) { n = avail; }; + let i: i32 = 0; + for (i < n) { + buf[i] = m.ptr[m.pos + i]; + i += 1; + }; + m.pos += n; + return n: size; +}; + +fn dynamicwrite_v(s: io.vstream, buf: []u8) (size | io.error) = { + let m: *dynamic_ctx = s: *dynamic_ctx; + let need: i32 = m.pos + buf.len; + if (need > m.cap) { dynamicgrow_v(m, need); }; + let i: i32 = 0; + for (i < buf.len) { + m.ptr[m.pos + i] = buf[i]; + i += 1; + }; + m.pos += buf.len; + if (m.pos > m.len) { m.len = m.pos; }; + return buf.len: size; +}; + +fn dynamicclose_v(s: io.vstream) (void | io.error) = { + let m: *dynamic_ctx = s: *dynamic_ctx; + if (m.cap > 0) { os.free(m.ptr: *void, m.cap: u64); }; + m.ptr = nil; + m.len = 0; + m.cap = 0; + m.pos = 0; + return; +}; + +// Mirror of memio.ww:203 dynamicgrow. Double-and-copy with floor at +// 8. Module-prefixed `_v` suffix vs the OLD memio.dynamicgrow keeps +// the cstage flat-TU private-fn scope from colliding (memio.ww:200 +// note + task #9). +fn dynamicgrow_v(d: *dynamic_ctx, need: i32) void = { + let newcap: i32 = d.cap; + if (newcap < 8) { newcap = 8; }; + for (newcap < need) { newcap *= 2; }; + let nbuf: *u8 = rt.malloc(newcap: u64): *u8; + let i: i32 = 0; + for (i < d.len) { + nbuf[i] = d.ptr[i]; + i += 1; + }; + if (d.cap > 0) { os.free(d.ptr: *void, d.cap: u64); }; + d.ptr = nbuf; + d.cap = newcap; +}; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index ac508459..dae54ebb 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -14325,6 +14325,249 @@ fn dynamicgrow(m: *state, need: i32) void = { m.cap = newcap; }; +// vstream — Hare-shaped vtable wrappers over memio. Project #94 +// fold-e2 (Option C, parallel API). +// +// Adds three constructors that return `io.vstream` (= `*io.vtable`, +// the Hare-shape from lib/io/stream.ww) alongside the pre-vtable +// memio.fixed / memio.dynamic / memio.dynamicfrom in memio.ww. The +// OLD surface stays untouched here — fold-eFinal (task #50) atomically +// flips the package shape: deletes the OLD constructors + callbacks, +// renames `_vstream` suffix off, and migrates the few callers. +// +// Hare's memio::fixed/dynamic return `stream` whose FIRST field IS +// `io::stream` (= `*vtable`). ww mirrors that intrusively: each ctx +// struct's first field is `vt: io.vtable` (the vtable embedded +// INLINE, not a pointer to it) so a heap-alloc'd `*fixed_ctx` is +// castable to `vstream = *vtable` via `&c.vt` — and the callbacks +// recover the outer ctx by casting the dispatch arg back to +// `*fixed_ctx`. Same intrusive shape as lib/bufio.stream over +// io.stream (bufio.ww:240-261) and lib/log.stdlogger over logger +// (log.ww:95-98). +// +// Cast workaround per #206: bare `&fn_name` does not type-check as +// a `(* | void)` field-init / let-binding (the structural +// `*fn(...)` value isn't accepted as the `*reader` named variant +// of the tagged slot). Explicit `(&fn_name): *io.` cast at +// each store site is the Hare-faithful minimum-touch route — same +// workaround test/wcc/775_io_vtable_run.c uses for the bare-vtable +// init. 8 cast tokens here (2 in fixed_vstream + 3 each in +// dynamic_vstream / dynamicfrom_vstream — fixed leaves closer +// void per Hare's fixed_vt, which lets st_close's void-arm return +// plain `void` with no callback needed). Casts drop out wholesale +// once #206 closes. +// +// ptr/len/cap kept flat (no `buf: []u8`) per the memio.state note +// (memio.ww:39): chained-dot writes through a state pointer into a +// slice subfield miscompile silently (related to the #195 family); +// the flat shape sidesteps it. dynamicfrom_vstream uses the slice's +// `cap` (NOT `len`) to track the allocated-capacity-to-free on +// close — mirrors the same OLD memio.dynamicfrom note (memio.ww:73). + +package memio; + +import io; +import os; +import rt; + +// fixed_ctx — heap-alloc'd state for fixed_vstream. `vt` at offset 0 +// for the intrusive vstream cast. +export type fixed_ctx = struct { + vt: io.vtable, + ptr: *u8, + len: i32, + cap: i32, + pos: i32, +}; + +// dynamic_ctx — heap-alloc'd state for dynamic_vstream / +// dynamicfrom_vstream. Same intrusive shape as fixed_ctx; the +// difference is the vtable wired (writer=dynamicwrite_v, +// closer=dynamicclose_v) and that the buffer can grow via +// dynamicgrow_v on write overflow. +export type dynamic_ctx = struct { + vt: io.vtable, + ptr: *u8, + len: i32, + cap: i32, + pos: i32, +}; + +// fixed_vstream — wire a vstream over a caller-supplied buffer. +// Writes never grow; they return 0 once `pos` reaches the end of +// the buffer (Hare returns `nomem` here; ww surfaces 0 to mirror +// the OLD memio.fixedwrite divergence — graduating to Hare's +// `nomem` return needs the widen-from-bare-nomem path that #173 +// blocks, deferred to eFinal). +// +// Mirrors ref/hare/memio/stream.ha:47. +// +// Three steps (vs Hare's single struct literal): alloc with vt +// zero-init (struct-lit `vt = local_vt` silently drops tagged-union +// fields past the first — sibling task; the `let zero` + post-alloc +// chained `c.vt.X = …` field-assigns through the *fixed_ctx pointer +// is the proven-working route, same shape lib/bufio.init uses on +// b.vtable). Field-assign of a TY_PTR/void variant through the +// chained pointer-into-struct is fine here (*fixed_ctx is a plain +// pointer-to-struct, not an aliased pointer; #195's "aliased-ptr +// receiver" carve-out doesn't bite). +export fn fixed_vstream(buf: []u8) (io.vstream | nomem) = { + let zero: io.vtable; + let c: *fixed_ctx = alloc(fixed_ctx{ + vt = zero, + ptr = buf.ptr, + len = buf.len, + cap = buf.len, + pos = 0, + })?; + c.vt.reader = (&fixedread_v): *io.reader; + c.vt.writer = (&fixedwrite_v): *io.writer; + return &c.vt; +}; + +// dynamic_vstream — wire a vstream with no initial buffer. Writes +// grow the backing allocation; [[io.st_close]] frees it. +// +// Mirrors ref/hare/memio/stream.ha:54. +export fn dynamic_vstream() (io.vstream | nomem) = { + let zero: io.vtable; + let c: *dynamic_ctx = alloc(dynamic_ctx{ + vt = zero, + ptr = nil, + len = 0, + cap = 0, + pos = 0, + })?; + c.vt.reader = (&dynread_v): *io.reader; + c.vt.writer = (&dynamicwrite_v): *io.writer; + c.vt.closer = (&dynamicclose_v): *io.closer; + return &c.vt; +}; + +// dynamicfrom_vstream — like [[dynamic_vstream]] but seeded with an +// existing slice. Ownership transfers; close frees `cap` bytes from +// the slice's allocated capacity (NOT logical length) — passing a +// half-filled append slice with len < cap and using only `buf.len` +// would under-free on close. +// +// Mirrors ref/hare/memio/stream.ha:65. +export fn dynamicfrom_vstream(buf: []u8) (io.vstream | nomem) = { + let zero: io.vtable; + let c: *dynamic_ctx = alloc(dynamic_ctx{ + vt = zero, + ptr = buf.ptr, + len = buf.len, + cap = buf.cap, + pos = 0, + })?; + c.vt.reader = (&dynread_v): *io.reader; + c.vt.writer = (&dynamicwrite_v): *io.writer; + c.vt.closer = (&dynamicclose_v): *io.closer; + return &c.vt; +}; + +// ---- vtable callbacks ---------------------------------------------------- + +// fixedread_v / dynread_v — recover the ctx from vstream's +// `*vtable` via the intrusive offset-0 cast. Separate fns (vs +// sharing one readfn) so each cast targets the matching ctx type +// — the Hare-side `s: *stream` cast in ref/hare/memio/stream.ha:103 +// has the same shape but only one ctx flavour. + +fn fixedread_v(s: io.vstream, buf: []u8) (size | io.eof | io.error) = { + let m: *fixed_ctx = s: *fixed_ctx; + if (m.pos >= m.len) { + let e: io.eof; + return e; + }; + let avail: i32 = m.len - m.pos; + let n: i32 = buf.len; + if (avail < n) { n = avail; }; + let i: i32 = 0; + for (i < n) { + buf[i] = m.ptr[m.pos + i]; + i += 1; + }; + m.pos += n; + return n: size; +}; + +fn fixedwrite_v(s: io.vstream, buf: []u8) (size | io.error) = { + let m: *fixed_ctx = s: *fixed_ctx; + if (m.pos >= m.len) { return 0: size; }; + let space: i32 = m.len - m.pos; + let n: i32 = buf.len; + if (space < n) { n = space; }; + let i: i32 = 0; + for (i < n) { + m.ptr[m.pos + i] = buf[i]; + i += 1; + }; + m.pos += n; + return n: size; +}; + +fn dynread_v(s: io.vstream, buf: []u8) (size | io.eof | io.error) = { + let m: *dynamic_ctx = s: *dynamic_ctx; + if (m.pos >= m.len) { + let e: io.eof; + return e; + }; + let avail: i32 = m.len - m.pos; + let n: i32 = buf.len; + if (avail < n) { n = avail; }; + let i: i32 = 0; + for (i < n) { + buf[i] = m.ptr[m.pos + i]; + i += 1; + }; + m.pos += n; + return n: size; +}; + +fn dynamicwrite_v(s: io.vstream, buf: []u8) (size | io.error) = { + let m: *dynamic_ctx = s: *dynamic_ctx; + let need: i32 = m.pos + buf.len; + if (need > m.cap) { dynamicgrow_v(m, need); }; + let i: i32 = 0; + for (i < buf.len) { + m.ptr[m.pos + i] = buf[i]; + i += 1; + }; + m.pos += buf.len; + if (m.pos > m.len) { m.len = m.pos; }; + return buf.len: size; +}; + +fn dynamicclose_v(s: io.vstream) (void | io.error) = { + let m: *dynamic_ctx = s: *dynamic_ctx; + if (m.cap > 0) { os.free(m.ptr: *void, m.cap: u64); }; + m.ptr = nil; + m.len = 0; + m.cap = 0; + m.pos = 0; + return; +}; + +// Mirror of memio.ww:203 dynamicgrow. Double-and-copy with floor at +// 8. Module-prefixed `_v` suffix vs the OLD memio.dynamicgrow keeps +// the cstage flat-TU private-fn scope from colliding (memio.ww:200 +// note + task #9). +fn dynamicgrow_v(d: *dynamic_ctx, need: i32) void = { + let newcap: i32 = d.cap; + if (newcap < 8) { newcap = 8; }; + for (newcap < need) { newcap *= 2; }; + let nbuf: *u8 = rt.malloc(newcap: u64): *u8; + let i: i32 = 0; + for (i < d.len) { + nbuf[i] = d.ptr[i]; + i += 1; + }; + if (d.cap > 0) { os.free(d.ptr: *void, d.cap: u64); }; + d.ptr = nbuf; + d.cap = newcap; +}; + // 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 8514de57..6fd80599 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -14325,6 +14325,249 @@ fn dynamicgrow(m: *state, need: i32) void = { m.cap = newcap; }; +// vstream — Hare-shaped vtable wrappers over memio. Project #94 +// fold-e2 (Option C, parallel API). +// +// Adds three constructors that return `io.vstream` (= `*io.vtable`, +// the Hare-shape from lib/io/stream.ww) alongside the pre-vtable +// memio.fixed / memio.dynamic / memio.dynamicfrom in memio.ww. The +// OLD surface stays untouched here — fold-eFinal (task #50) atomically +// flips the package shape: deletes the OLD constructors + callbacks, +// renames `_vstream` suffix off, and migrates the few callers. +// +// Hare's memio::fixed/dynamic return `stream` whose FIRST field IS +// `io::stream` (= `*vtable`). ww mirrors that intrusively: each ctx +// struct's first field is `vt: io.vtable` (the vtable embedded +// INLINE, not a pointer to it) so a heap-alloc'd `*fixed_ctx` is +// castable to `vstream = *vtable` via `&c.vt` — and the callbacks +// recover the outer ctx by casting the dispatch arg back to +// `*fixed_ctx`. Same intrusive shape as lib/bufio.stream over +// io.stream (bufio.ww:240-261) and lib/log.stdlogger over logger +// (log.ww:95-98). +// +// Cast workaround per #206: bare `&fn_name` does not type-check as +// a `(* | void)` field-init / let-binding (the structural +// `*fn(...)` value isn't accepted as the `*reader` named variant +// of the tagged slot). Explicit `(&fn_name): *io.` cast at +// each store site is the Hare-faithful minimum-touch route — same +// workaround test/wcc/775_io_vtable_run.c uses for the bare-vtable +// init. 8 cast tokens here (2 in fixed_vstream + 3 each in +// dynamic_vstream / dynamicfrom_vstream — fixed leaves closer +// void per Hare's fixed_vt, which lets st_close's void-arm return +// plain `void` with no callback needed). Casts drop out wholesale +// once #206 closes. +// +// ptr/len/cap kept flat (no `buf: []u8`) per the memio.state note +// (memio.ww:39): chained-dot writes through a state pointer into a +// slice subfield miscompile silently (related to the #195 family); +// the flat shape sidesteps it. dynamicfrom_vstream uses the slice's +// `cap` (NOT `len`) to track the allocated-capacity-to-free on +// close — mirrors the same OLD memio.dynamicfrom note (memio.ww:73). + +package memio; + +import io; +import os; +import rt; + +// fixed_ctx — heap-alloc'd state for fixed_vstream. `vt` at offset 0 +// for the intrusive vstream cast. +export type fixed_ctx = struct { + vt: io.vtable, + ptr: *u8, + len: i32, + cap: i32, + pos: i32, +}; + +// dynamic_ctx — heap-alloc'd state for dynamic_vstream / +// dynamicfrom_vstream. Same intrusive shape as fixed_ctx; the +// difference is the vtable wired (writer=dynamicwrite_v, +// closer=dynamicclose_v) and that the buffer can grow via +// dynamicgrow_v on write overflow. +export type dynamic_ctx = struct { + vt: io.vtable, + ptr: *u8, + len: i32, + cap: i32, + pos: i32, +}; + +// fixed_vstream — wire a vstream over a caller-supplied buffer. +// Writes never grow; they return 0 once `pos` reaches the end of +// the buffer (Hare returns `nomem` here; ww surfaces 0 to mirror +// the OLD memio.fixedwrite divergence — graduating to Hare's +// `nomem` return needs the widen-from-bare-nomem path that #173 +// blocks, deferred to eFinal). +// +// Mirrors ref/hare/memio/stream.ha:47. +// +// Three steps (vs Hare's single struct literal): alloc with vt +// zero-init (struct-lit `vt = local_vt` silently drops tagged-union +// fields past the first — sibling task; the `let zero` + post-alloc +// chained `c.vt.X = …` field-assigns through the *fixed_ctx pointer +// is the proven-working route, same shape lib/bufio.init uses on +// b.vtable). Field-assign of a TY_PTR/void variant through the +// chained pointer-into-struct is fine here (*fixed_ctx is a plain +// pointer-to-struct, not an aliased pointer; #195's "aliased-ptr +// receiver" carve-out doesn't bite). +export fn fixed_vstream(buf: []u8) (io.vstream | nomem) = { + let zero: io.vtable; + let c: *fixed_ctx = alloc(fixed_ctx{ + vt = zero, + ptr = buf.ptr, + len = buf.len, + cap = buf.len, + pos = 0, + })?; + c.vt.reader = (&fixedread_v): *io.reader; + c.vt.writer = (&fixedwrite_v): *io.writer; + return &c.vt; +}; + +// dynamic_vstream — wire a vstream with no initial buffer. Writes +// grow the backing allocation; [[io.st_close]] frees it. +// +// Mirrors ref/hare/memio/stream.ha:54. +export fn dynamic_vstream() (io.vstream | nomem) = { + let zero: io.vtable; + let c: *dynamic_ctx = alloc(dynamic_ctx{ + vt = zero, + ptr = nil, + len = 0, + cap = 0, + pos = 0, + })?; + c.vt.reader = (&dynread_v): *io.reader; + c.vt.writer = (&dynamicwrite_v): *io.writer; + c.vt.closer = (&dynamicclose_v): *io.closer; + return &c.vt; +}; + +// dynamicfrom_vstream — like [[dynamic_vstream]] but seeded with an +// existing slice. Ownership transfers; close frees `cap` bytes from +// the slice's allocated capacity (NOT logical length) — passing a +// half-filled append slice with len < cap and using only `buf.len` +// would under-free on close. +// +// Mirrors ref/hare/memio/stream.ha:65. +export fn dynamicfrom_vstream(buf: []u8) (io.vstream | nomem) = { + let zero: io.vtable; + let c: *dynamic_ctx = alloc(dynamic_ctx{ + vt = zero, + ptr = buf.ptr, + len = buf.len, + cap = buf.cap, + pos = 0, + })?; + c.vt.reader = (&dynread_v): *io.reader; + c.vt.writer = (&dynamicwrite_v): *io.writer; + c.vt.closer = (&dynamicclose_v): *io.closer; + return &c.vt; +}; + +// ---- vtable callbacks ---------------------------------------------------- + +// fixedread_v / dynread_v — recover the ctx from vstream's +// `*vtable` via the intrusive offset-0 cast. Separate fns (vs +// sharing one readfn) so each cast targets the matching ctx type +// — the Hare-side `s: *stream` cast in ref/hare/memio/stream.ha:103 +// has the same shape but only one ctx flavour. + +fn fixedread_v(s: io.vstream, buf: []u8) (size | io.eof | io.error) = { + let m: *fixed_ctx = s: *fixed_ctx; + if (m.pos >= m.len) { + let e: io.eof; + return e; + }; + let avail: i32 = m.len - m.pos; + let n: i32 = buf.len; + if (avail < n) { n = avail; }; + let i: i32 = 0; + for (i < n) { + buf[i] = m.ptr[m.pos + i]; + i += 1; + }; + m.pos += n; + return n: size; +}; + +fn fixedwrite_v(s: io.vstream, buf: []u8) (size | io.error) = { + let m: *fixed_ctx = s: *fixed_ctx; + if (m.pos >= m.len) { return 0: size; }; + let space: i32 = m.len - m.pos; + let n: i32 = buf.len; + if (space < n) { n = space; }; + let i: i32 = 0; + for (i < n) { + m.ptr[m.pos + i] = buf[i]; + i += 1; + }; + m.pos += n; + return n: size; +}; + +fn dynread_v(s: io.vstream, buf: []u8) (size | io.eof | io.error) = { + let m: *dynamic_ctx = s: *dynamic_ctx; + if (m.pos >= m.len) { + let e: io.eof; + return e; + }; + let avail: i32 = m.len - m.pos; + let n: i32 = buf.len; + if (avail < n) { n = avail; }; + let i: i32 = 0; + for (i < n) { + buf[i] = m.ptr[m.pos + i]; + i += 1; + }; + m.pos += n; + return n: size; +}; + +fn dynamicwrite_v(s: io.vstream, buf: []u8) (size | io.error) = { + let m: *dynamic_ctx = s: *dynamic_ctx; + let need: i32 = m.pos + buf.len; + if (need > m.cap) { dynamicgrow_v(m, need); }; + let i: i32 = 0; + for (i < buf.len) { + m.ptr[m.pos + i] = buf[i]; + i += 1; + }; + m.pos += buf.len; + if (m.pos > m.len) { m.len = m.pos; }; + return buf.len: size; +}; + +fn dynamicclose_v(s: io.vstream) (void | io.error) = { + let m: *dynamic_ctx = s: *dynamic_ctx; + if (m.cap > 0) { os.free(m.ptr: *void, m.cap: u64); }; + m.ptr = nil; + m.len = 0; + m.cap = 0; + m.pos = 0; + return; +}; + +// Mirror of memio.ww:203 dynamicgrow. Double-and-copy with floor at +// 8. Module-prefixed `_v` suffix vs the OLD memio.dynamicgrow keeps +// the cstage flat-TU private-fn scope from colliding (memio.ww:200 +// note + task #9). +fn dynamicgrow_v(d: *dynamic_ctx, need: i32) void = { + let newcap: i32 = d.cap; + if (newcap < 8) { newcap = 8; }; + for (newcap < need) { newcap *= 2; }; + let nbuf: *u8 = rt.malloc(newcap: u64): *u8; + let i: i32 = 0; + for (i < d.len) { + nbuf[i] = d.ptr[i]; + i += 1; + }; + if (d.cap > 0) { os.free(d.ptr: *void, d.cap: u64); }; + d.ptr = nbuf; + d.cap = newcap; +}; + // selfhost/cmd/wcc/cgenutil.ww — split out of cgen.ww. // // General helpers used across cgenexpr / cgenstmt / cgendecl: diff --git a/test/wcc/776_memio_vstream_run.c b/test/wcc/776_memio_vstream_run.c new file mode 100644 index 00000000..2c77290f --- /dev/null +++ b/test/wcc/776_memio_vstream_run.c @@ -0,0 +1,398 @@ +/* + * 776_memio_vstream_run — project #94 fold-e2 sentinel. Pins the + * additive lib/memio/vstream.ww Option C parallel API: heap-alloc'd + * ctx structs (fixed_ctx / dynamic_ctx) with `vt: io.vtable` as the + * first field for the intrusive cast, three constructors + * (fixed_vstream / dynamic_vstream / dynamicfrom_vstream) that + * return `(io.vstream | nomem)`, and the matching read/write/close + * callbacks that recover the outer ctx via a vstream→*ctx pointer + * cast. Coexists with the pre-vtable memio.fixed/dynamic/dynamicfrom + * surface in memio.ww (fold-eFinal, task #50, retires the latter). + * + * Each row imports memio + io, calls one or more constructors, drives + * the io.st_read/st_write/st_close dispatchers, and asserts an exit + * constant. Both stages run; cs.s == ww.s byte-id on every row. + * + * row | what it pins + * --------------------------+-------------------------------------- + * fixed_read_5 | fixed_vstream + io.st_read of 5 bytes + * | over an 8-byte source. Asserts the + * | return is `size` and equal to 5, and + * | that out[0]/out[4] mirror the buffer. + * | Pins the full intrusive shape (alloc + * | fixed_ctx → return &c.vt; dispatcher + * | recovers via s: *fixed_ctx). + * dynamic_write_grow | dynamic_vstream + io.st_write of 3 + * | bytes. Asserts the write reports 3 + * | and that the dynamicgrow_v path + * | allocated the backing buffer (initial + * | cap=0 forces growth on the first + * | write). Closes via io.st_close to + * | exercise dynamicclose_v's free. + * dynamicfrom_alt_rw | dynamicfrom_vstream seeded with a + * | 4-byte slice. Alternates st_read + * | (4 bytes back) → st_write (2 bytes, + * | grows past cap=4 → realloc). Asserts + * | both paths report their lengths. + * | Single ctx flavour, two dispatcher + * | paths through the same vtable. + * branched_fixed | branched callee per #105: two + * | fixed_vstream instances with + * | different buffer sizes (3 and 5), + * | runtime-selected by the exit code + * | nudge. st_read on each returns the + * | matching size. Catches a constant- + * | fold mistake in the dispatch path + * | (mirror of 775's branched_readers + * | row for the vtable-init side). + * + * IMPORT-ORDER WORKAROUND (pre-existing wwstage gap, NOT introduced + * here): every row places `import os;` FIRST. ww_ww's combined.ww + * concatenation follows the import-discovery order; an os-late + * ordering (io → rt → os) trips the wwstage checker on os.tryread / + * trywrite / tryopen's bare `return r;` over a tagged return type + * (3 false-positive "return: not assignable (i64 → )"/"(i32 → )" + * errors). The os-first ordering matches selfhost's combined.ww + * (time → os → rt → …) where the checker resolves cleanly. The 980 + * memio_run / 990 / 995 byte-id gates do not hit this path because + * they either drive cs-only (980) or operate on a much wider type + * surface (990/995). Filed as a sibling task; bare `os` import in a + * small probe context is what surfaces it. Workaround drops out once + * the wwstage checker stops ordering-sensitively on bare-int return + * to tagged-int union. + * + * SIBLINGS (filed inline, NOT fixed here — fold-e2 is purely + * additive over fold-e1's frozen io.* surface): + * + * - WWSTAGE-IMPORT-ORDER: `import os;` must appear before `import + * memio;` / `import io;` in a small probe context. ww_ww's + * combined.ww concat order trips the wwstage checker's + * `(i64 | T)` / `(i32 | T)` return-assignability when bare + * int-returning fns (os.tryread / trywrite / tryopen) are + * checked before certain ordering-sensitive pre-resolved types. + * cstage accepts unconditionally. Symmetric to the wwstage + * ordering gaps already filed as #189 / #190 / #202. + * + * - #173 (TRY-on-tagged-return both-stages broken) blocks + * fixed_write_v from returning Hare's `nomem` when full; + * vstream.ww surfaces 0 instead (memio.ww:155 OLD divergence). + * The probe doesn't pin Hare-equivalent nomem behaviour; + * fold-eFinal can graduate once #173 closes. + * + * - #195 (cgassign N_DOT TK_ASSIGN on aliased-ptr receiver): + * would bite if vstream.ww used chained `c.vt.reader = …` + * stores through the alloc'd ctx pointer. We sidestep via a + * local `let vt: io.vtable;` + field-assigns + struct-lit + * `alloc(fixed_ctx{vt=vt, …})`. Mirrors memio.ww:39's flat- + * slice-fields workaround. eFinal can collapse once #195 closes. + * + * GATE POLARITY: must stay GREEN. A red here means vstream.ww + * regressed, the alloc-struct-? path miscompiled, the intrusive + * vstream→*ctx cast miscomputed offsets, 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[] = { + { "fixed_read_5", + "package main;\n" + "import os;\n" + "import memio;\n" + "import io;\n" + "export fn main() i32 = {\n" + " let buf: [8]u8;\n" + " buf[0] = 65u8; buf[1] = 66u8; buf[2] = 67u8; buf[3] = 68u8;\n" + " buf[4] = 69u8; buf[5] = 70u8; buf[6] = 71u8; buf[7] = 72u8;\n" + " let r = memio.fixed_vstream(buf[0:8]);\n" + " let s: io.vstream = nil: *io.vtable;\n" + " match (r) {\n" + " case let v: io.vstream => { s = v; };\n" + " case nomem => { return 50; };\n" + " };\n" + " let out: [5]u8;\n" + " let n: size = 0;\n" + " let rd = io.st_read(s, out[0:5]);\n" + " if (rd is size) { n = rd as size; };\n" + " if (n == 5: size && out[0] == 65u8 && out[4] == 69u8) { return 42; };\n" + " return 99;\n" + "};\n", + 42, + STAGE_CS | STAGE_WW, 1 }, + { "dynamic_write_grow", + "package main;\n" + "import os;\n" + "import memio;\n" + "import io;\n" + "export fn main() i32 = {\n" + " let r = memio.dynamic_vstream();\n" + " let s: io.vstream = nil: *io.vtable;\n" + " match (r) {\n" + " case let v: io.vstream => { s = v; };\n" + " case nomem => { return 50; };\n" + " };\n" + " let src: [3]u8;\n" + " src[0] = 88u8; src[1] = 89u8; src[2] = 90u8;\n" + " let wr = io.st_write(s, src[0:3]);\n" + " let n: size = 0;\n" + " if (wr is size) { n = wr as size; };\n" + " let cl = io.st_close(s);\n" + " if (cl is void && n == 3: size) { return 43; };\n" + " return 99;\n" + "};\n", + 43, + STAGE_CS | STAGE_WW, 1 }, + { "dynamicfrom_alt_rw", + "package main;\n" + "import os;\n" + "import memio;\n" + "import io;\n" + "export fn main() i32 = {\n" + " let seed: [4]u8;\n" + " seed[0] = 1u8; seed[1] = 2u8; seed[2] = 3u8; seed[3] = 4u8;\n" + " let r = memio.dynamicfrom_vstream(seed[0:4]);\n" + " let s: io.vstream = nil: *io.vtable;\n" + " match (r) {\n" + " case let v: io.vstream => { s = v; };\n" + " case nomem => { return 50; };\n" + " };\n" + " let out: [4]u8;\n" + " let rd = io.st_read(s, out[0:4]);\n" + " let rn: size = 0;\n" + " if (rd is size) { rn = rd as size; };\n" + " let extra: [2]u8;\n" + " extra[0] = 99u8; extra[1] = 100u8;\n" + " let wr = io.st_write(s, extra[0:2]);\n" + " let wn: size = 0;\n" + " if (wr is size) { wn = wr as size; };\n" + " if (rn == 4: size && wn == 2: size && out[0] == 1u8 && out[3] == 4u8) { return 44; };\n" + " return 99;\n" + "};\n", + 44, + STAGE_CS | STAGE_WW, 1 }, + { "branched_fixed", + "package main;\n" + "import os;\n" + "import memio;\n" + "import io;\n" + "export fn main() i32 = {\n" + " let a: [3]u8;\n" + " a[0] = 10u8; a[1] = 11u8; a[2] = 12u8;\n" + " let b: [5]u8;\n" + " b[0] = 20u8; b[1] = 21u8; b[2] = 22u8; b[3] = 23u8; b[4] = 24u8;\n" + " let ra = memio.fixed_vstream(a[0:3]);\n" + " let rb = memio.fixed_vstream(b[0:5]);\n" + " let sa: io.vstream = nil: *io.vtable;\n" + " let sb: io.vstream = nil: *io.vtable;\n" + " match (ra) {\n" + " case let v: io.vstream => { sa = v; };\n" + " case nomem => { return 50; };\n" + " };\n" + " match (rb) {\n" + " case let v: io.vstream => { sb = v; };\n" + " case nomem => { return 51; };\n" + " };\n" + " let outa: [3]u8;\n" + " let outb: [5]u8;\n" + " let rda = io.st_read(sa, outa[0:3]);\n" + " let rdb = io.st_read(sb, outb[0:5]);\n" + " let na: size = 0;\n" + " let nb: size = 0;\n" + " if (rda is size) { na = rda as size; };\n" + " if (rdb is size) { nb = rdb as size; };\n" + " if (na == 3: size && nb == 5: size && outa[2] == 12u8 && outb[4] == 24u8) { return 45; };\n" + " return 99;\n" + "};\n", + 45, + STAGE_CS | STAGE_WW, 1 }, +}; + +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. */ +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/mvs_%d_d_%d", getpid(), seq); + snprintf(base, sizeof base, "main776"); + 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). */ +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/mvs_%d_c_%d", getpid(), seq); + snprintf(tdw, sizeof tdw, "/tmp/mvs_%d_w_%d", getpid(), seq); + snprintf(base, sizeof base, "main776"); + 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, + "memio_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, + "memio_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, + "memio_vstream_run[byte-id][%s]: cstage vs wwstage asm differs\n", + rows[i].label); + fail++; + } + } + } + } + + if (!wwpresent) + fprintf(stderr, "memio_vstream_run: skip wwstage (no %s)\n", wdrv); + + if (fail) { + fprintf(stderr, "memio_vstream_run: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("memio_vstream_run: %d/%d ok\n", total, total); + return 0; +}