From 90ed913160e709e6a8cdf17b99f004b95a841aba Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 29 May 2026 07:28:59 +0900 Subject: [PATCH] lib/io: port ref/hare/io/stream.ha vtable surface (#94 fold-e1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Additive: keeps lib/io/io.ww's pre-vtable `stream` struct + `read`/`write`/`close` wrappers (fold-e2-eN migrates the legacy surface to vtable-backed implementations and retires it). lib/io/stream.ww — vtable struct (reader/writer/closer slots, spelled `(*T | void)` per #192 — ww parser rejects `nullable *T`), vstream = *vtable, and the st_read/st_write/st_close dispatchers per ref/hare/io/stream.ha:33-68. Void-arm `return e;` chains two direct widens: concrete `errors.unsupported` → `error` (#199 α) then `error` → (size|eof|error) (#205 NAMED-variant nominal at tagged→tagged subset). Hare's `?`-propagating st_close collapses to a direct `return (*c)(s);` because #173 is still open; the surface stays Hare-shaped. lib/io/types.ww — retarget reader/writer/closer fn-aliases from *stream to vstream. Extend `error` union to include `errors.unsupported` explicitly (no spread — per ken's #204-block the wrapper-vs-flatten layout asymmetry would mis-widen; the deferred fix is filed as #199b layout-extension). test/wcc/775_io_vtable_run.c — 7-row sentinel: reader/writer/ closer × {set, void} happy paths + branched-callee runtime. Rows verify the call runs + the constant exit; the void-arm rows do NOT inspect the resulting variant tag (deferred #199b wrapped-slot tag-remap mis-routes to dst tag 0). Cstage and wwstage emit byte-identical asm on every row. test/wcc/768_io_types_run.c — track the alias retarget; rows now build a vtable, pass `&vt` (= vstream), and call through the fn-VALUE param shape. Combined.ww regen for w6c + wwdump per #110: lib/errors lands transitively via the new `import errors;` in types.ww. Sibling filed inline (NOT fixed): the checker rejects bare `&fn_name` / `let p: *alias = &fn` assignment to a `(*alias | void)` field — the structural `*fn(...)` value isn't accepted as the `*alias` NAMED variant. Both stages reject. Probes route around via explicit `(&fn): *io.reader` cast at each vtable-field assignment. --- Makefile | 11 +- lib/io/stream.ww | 95 +++++++ lib/io/types.ww | 43 +-- selfhost/cmd/w6c/main.combined.ww | 167 +++++++++-- selfhost/cmd/wwdump/main.combined.ww | 167 +++++++++-- test/wcc/768_io_types_run.c | 40 +-- test/wcc/775_io_vtable_run.c | 400 +++++++++++++++++++++++++++ 7 files changed, 856 insertions(+), 67 deletions(-) create mode 100644 lib/io/stream.ww create mode 100644 test/wcc/775_io_vtable_run.c diff --git a/Makefile b/Makefile index fac966fc..6ef03122 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/io/io.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/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/io/io.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/fmt/fmt.ww \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) @mkdir -p $(BIN)/w6c_ww.d @@ -325,6 +325,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_typeassert_nonident \ $(BIN)/test_isas_spread_variant \ $(BIN)/test_tagged_widen_named_variant \ + $(BIN)/test_io_vtable_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 \ @@ -681,6 +682,12 @@ $(BIN)/test_tagged_widen_named_variant: test/wcc/774_tagged_widen_named_variant. $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_io_vtable_run: test/wcc/775_io_vtable_run.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_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/io/stream.ww b/lib/io/stream.ww new file mode 100644 index 00000000..06186002 --- /dev/null +++ b/lib/io/stream.ww @@ -0,0 +1,95 @@ +// stream — Hare-shaped vtable port. Project #94 fold-e1. +// +// Adds the additive vtable surface alongside lib/io/io.ww's pre-vtable +// stream struct + read/write/close wrappers (which fold-e2-eN migrate +// to vtable-backed implementations and then retire). Three exports: +// +// vtable a struct of optional fn-pointer slots — reader/writer/ +// closer per ref/hare/io/stream.ha:36-42. Hare spells the +// nullable as `nullable *T`; ww parser rejects that +// spelling (#192), so each slot is a `(*T | void)` tagged +// union (Plan-9-lean per the Nullable kept ruling; opt-in +// at optionality boundaries only). +// vstream `*vtable` alias matching Hare's `stream = *vtable` +// (ref/hare/io/stream.ha:33). Named `vstream` rather than +// `stream` because lib/io/io.ww still owns the legacy +// `stream` struct name until fold-eN collapses both. +// st_read / +// st_write / +// st_close the three dispatchers per ref/hare/io/stream.ha:44-68. +// Each `match`es the matching vtable slot, returns +// `errors.unsupported` (widened twice) when the slot is +// void, otherwise calls through the fn pointer. +// +// Fold-e1 cgen/checker dependencies (all closed at HEAD): #191 +// (N_DOT on aliased-ptr receiver — `s.reader` resolves through the +// `vstream = *vtable` alias), #198 (`is` on cross-module variant +// tag), #199 α (concrete→tagged widen via NAMED-variant nominal +// match, used by `let e: error = u;`), #200 (`as` payload-extract), +// #201 (return-of-tagged-call forwards instead of rewinds), #205 +// (tagged→tagged subset accepts NAMED-variant nominal match, used +// by `return e;` where ret type contains `error` as a direct +// variant). Two direct widens compose for the void-arm `return`: +// concrete `errors.unsupported` → tagged `error` via #199 α, then +// tagged `error` → tagged `(size | eof | error)` via #205. +// +// Deferrals (drew-signed): `seeker` lands with fold-e2 once `off` +// + `whence` plug into the signature; Hare's `?`-propagating +// `st_close` body (`c(s)?;`) collapses to a direct `return (*c)(s);` +// here because #173 (TRY-on-tagged-return both-stages broken) is +// still open — the dispatcher's surface stays Hare-shaped, only the +// internal try-prop is omitted. + +package io; + +// ref/hare/io/stream.ha:36-42. Nullable spelling per #192 (ww parser +// rejects `nullable *T`); each slot is `(*T | void)`. +export type vtable = struct { + reader: (*reader | void), + writer: (*writer | void), + closer: (*closer | void), +}; + +// ref/hare/io/stream.ha:33. `vstream` not `stream` while io.ww still +// owns the legacy `stream` struct name. +export type vstream = *vtable; + +// ref/hare/io/stream.ha:44-50. The void-arm `return e;` chains two +// direct widens: `errors.unsupported → error` via #199 α, then +// `error → (size | eof | error)` via #205. The call-arm spells the +// fn-ptr call explicitly per #193 (ww requires `(*r)(args)` rather +// than implicit `r(args)`). +export fn st_read(s: vstream, buf: []u8) (size | eof | error) = { + match (s.reader) { + case void => { + let u: errors.unsupported; + let e: error = u; + return e; + }; + case let r: *reader => return (*r)(s, buf); + }; +}; + +// ref/hare/io/stream.ha:53-59. +export fn st_write(s: vstream, buf: []u8) (size | error) = { + match (s.writer) { + case void => { + let u: errors.unsupported; + let e: error = u; + return e; + }; + case let w: *writer => return (*w)(s, buf); + }; +}; + +// ref/hare/io/stream.ha:62-68. Hare propagates the close error via +// `c(s)?;` then falls through to the void arm; ww collapses to a +// direct return because #173 (TRY-on-tagged-return) is still open. +// The surface (void | error) matches; only the internal try-prop +// is omitted. +export fn st_close(s: vstream) (void | error) = { + match (s.closer) { + case void => return void; + case let c: *closer => return (*c)(s); + }; +}; diff --git a/lib/io/types.ww b/lib/io/types.ww index 807ad11c..b79fbe35 100644 --- a/lib/io/types.ww +++ b/lib/io/types.ww @@ -1,18 +1,25 @@ // types — error union, mode/whence enums, reader/writer/closer -// fn-type aliases. Project #94 fold-1 step (d) (drew Fold D). +// fn-type aliases. Project #94 fold-1 step (d) (drew Fold D); the +// fn-aliases re-targeted from *stream → vstream as part of fold-e1 +// (vtable port in stream.ww). // // Hare splits the io module across stream.ha + types.ha and ww does // the same: lib/io/io.ww owns the (pre-vtable) `stream`, `eof`, and -// `underread` shapes; this file owns the surrounding port. Both -// files share `package io;` so forward refs (e.g. reader → stream) -// resolve via the dir-enum concat order. +// `underread` shapes; lib/io/stream.ww owns the Hare vtable port +// (`vtable`, `vstream`, `st_read`/`st_write`/`st_close` dispatch); +// this file owns the surrounding port. The three files share +// `package io;` so cross-file refs resolve via the dir-enum concat +// order (io.ww < stream.ww < types.ww — `vstream` lands before the +// reader/writer/closer aliases below). // // Deferrals (drew-signed): `handle`, `seeker`, `copier`, `strerror` -// land with fold-e (need the `handle` sum). Hare's `EOF = done` +// land with fold-e2+ (need the `handle` sum). Hare's `EOF = done` // stays as `io.eof` until the `done` singleton ships (#93). package io; +import errors; + // ref/hare/io/types.ha:29-34. RDWR is spelled `3` rather than Hare's // `READ | WRITE` because the ww parser doesn't fold expressions in // enum value positions; the value SSoT stays the same bitfield. @@ -24,7 +31,7 @@ export type mode = enum u8 { }; // ref/hare/io/types.ha:37-41. Hare leaves the underlying implicit; -// ww requires one. i32 matches the off type that fold-e will plug +// ww requires one. i32 matches the off type that fold-e2 will plug // into the seeker signature. export type whence = enum i32 { SET = 0, @@ -32,20 +39,24 @@ export type whence = enum i32 { END = 2, }; -// ref/hare/io/types.ha:11 spreads `errors::error` into the union; -// lib/errors isn't ported yet, so the union carries only the tags -// observable in this fold — `underread` (from io.ww) and the -// predeclared `nomem` (#29, type.c:72 / check.ww:78). -export type error = !(underread | nomem); +// ref/hare/io/types.ha:11 spreads `...errors::error` into the union; +// ww enumerates the tags explicitly (ken's #204-block — spread of an +// errors-side tagged into this union triggers a wrapper-vs-flatten +// layout mismatch; #199b layout-extension is the deferred fix). The +// st_read/st_write dispatchers in stream.ww return +// `errors.unsupported` when the matching vtable slot is unset, so the +// variant lands here directly. +export type error = !(errors.unsupported | underread | nomem); // ref/hare/io/types.ha:46. `eof` (not Hare's `done`) per the io.ww:8 -// rationale; lifts to `done` with #93. `*stream` forward-refs the -// pre-vtable struct in io.ww — same cross-file pattern Hare uses. -export type reader = fn(s: *stream, buf: []u8) (size | eof | error); +// rationale; lifts to `done` with #93. `vstream` forward-refs the +// `*vtable` alias in stream.ww — the dispatcher receives the vtable +// pointer directly (Hare's stream.ha:33 + types.ha:46 collapse). +export type reader = fn(s: vstream, buf: []u8) (size | eof | error); // ref/hare/io/types.ha:51. No `const` qualifier — ww has none, and // lib/io/io.ww:30 + lib/sort/sort.ww:18 drop it the same way. -export type writer = fn(s: *stream, buf: []u8) (size | error); +export type writer = fn(s: vstream, buf: []u8) (size | error); // ref/hare/io/types.ha:55. -export type closer = fn(s: *stream) (void | error); +export type closer = fn(s: vstream) (void | error); diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 81d99bfe..ac508459 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -13921,21 +13921,152 @@ export fn close(s: *stream) (void | closed) = { return s.close(s); }; +// stream — Hare-shaped vtable port. Project #94 fold-e1. +// +// Adds the additive vtable surface alongside lib/io/io.ww's pre-vtable +// stream struct + read/write/close wrappers (which fold-e2-eN migrate +// to vtable-backed implementations and then retire). Three exports: +// +// vtable a struct of optional fn-pointer slots — reader/writer/ +// closer per ref/hare/io/stream.ha:36-42. Hare spells the +// nullable as `nullable *T`; ww parser rejects that +// spelling (#192), so each slot is a `(*T | void)` tagged +// union (Plan-9-lean per the Nullable kept ruling; opt-in +// at optionality boundaries only). +// vstream `*vtable` alias matching Hare's `stream = *vtable` +// (ref/hare/io/stream.ha:33). Named `vstream` rather than +// `stream` because lib/io/io.ww still owns the legacy +// `stream` struct name until fold-eN collapses both. +// st_read / +// st_write / +// st_close the three dispatchers per ref/hare/io/stream.ha:44-68. +// Each `match`es the matching vtable slot, returns +// `errors.unsupported` (widened twice) when the slot is +// void, otherwise calls through the fn pointer. +// +// Fold-e1 cgen/checker dependencies (all closed at HEAD): #191 +// (N_DOT on aliased-ptr receiver — `s.reader` resolves through the +// `vstream = *vtable` alias), #198 (`is` on cross-module variant +// tag), #199 α (concrete→tagged widen via NAMED-variant nominal +// match, used by `let e: error = u;`), #200 (`as` payload-extract), +// #201 (return-of-tagged-call forwards instead of rewinds), #205 +// (tagged→tagged subset accepts NAMED-variant nominal match, used +// by `return e;` where ret type contains `error` as a direct +// variant). Two direct widens compose for the void-arm `return`: +// concrete `errors.unsupported` → tagged `error` via #199 α, then +// tagged `error` → tagged `(size | eof | error)` via #205. +// +// Deferrals (drew-signed): `seeker` lands with fold-e2 once `off` +// + `whence` plug into the signature; Hare's `?`-propagating +// `st_close` body (`c(s)?;`) collapses to a direct `return (*c)(s);` +// here because #173 (TRY-on-tagged-return both-stages broken) is +// still open — the dispatcher's surface stays Hare-shaped, only the +// internal try-prop is omitted. + +package io; + +// ref/hare/io/stream.ha:36-42. Nullable spelling per #192 (ww parser +// rejects `nullable *T`); each slot is `(*T | void)`. +export type vtable = struct { + reader: (*reader | void), + writer: (*writer | void), + closer: (*closer | void), +}; + +// ref/hare/io/stream.ha:33. `vstream` not `stream` while io.ww still +// owns the legacy `stream` struct name. +export type vstream = *vtable; + +// ref/hare/io/stream.ha:44-50. The void-arm `return e;` chains two +// direct widens: `errors.unsupported → error` via #199 α, then +// `error → (size | eof | error)` via #205. The call-arm spells the +// fn-ptr call explicitly per #193 (ww requires `(*r)(args)` rather +// than implicit `r(args)`). +export fn st_read(s: vstream, buf: []u8) (size | eof | error) = { + match (s.reader) { + case void => { + let u: errors.unsupported; + let e: error = u; + return e; + }; + case let r: *reader => return (*r)(s, buf); + }; +}; + +// ref/hare/io/stream.ha:53-59. +export fn st_write(s: vstream, buf: []u8) (size | error) = { + match (s.writer) { + case void => { + let u: errors.unsupported; + let e: error = u; + return e; + }; + case let w: *writer => return (*w)(s, buf); + }; +}; + +// ref/hare/io/stream.ha:62-68. Hare propagates the close error via +// `c(s)?;` then falls through to the void arm; ww collapses to a +// direct return because #173 (TRY-on-tagged-return) is still open. +// The surface (void | error) matches; only the internal try-prop +// is omitted. +export fn st_close(s: vstream) (void | error) = { + match (s.closer) { + case void => return void; + case let c: *closer => return (*c)(s); + }; +}; + +// errors — domain-agnostic error types. Mirrors ref/hare/errors/. +// +// Named-void tagged-union variants, so `(T | errors.invalid | ...)` +// composes with every other module's error surface at the tag level. +// Per-domain errors (io.eof, io.closed, io.underread, strconv.overflow, +// ...) live in their own modules; this module ships only the generic +// conditions Hare's errors:: exports. +// +// Subset shipped today; add more from Hare's errors/common.ha as callers +// need them. + +// A function was called with an invalid combination of arguments. +package errors; + +export type invalid = !void; + +// The user does not have permission to use this resource. +export type noaccess = !void; + +// An entry was requested which does not exist. +export type noentry = !void; + +// An attempt was made to create a resource which already exists. +export type exists = !void; + +// The requested operation is not supported. +export type unsupported = !void; + // types — error union, mode/whence enums, reader/writer/closer -// fn-type aliases. Project #94 fold-1 step (d) (drew Fold D). +// fn-type aliases. Project #94 fold-1 step (d) (drew Fold D); the +// fn-aliases re-targeted from *stream → vstream as part of fold-e1 +// (vtable port in stream.ww). // // Hare splits the io module across stream.ha + types.ha and ww does // the same: lib/io/io.ww owns the (pre-vtable) `stream`, `eof`, and -// `underread` shapes; this file owns the surrounding port. Both -// files share `package io;` so forward refs (e.g. reader → stream) -// resolve via the dir-enum concat order. +// `underread` shapes; lib/io/stream.ww owns the Hare vtable port +// (`vtable`, `vstream`, `st_read`/`st_write`/`st_close` dispatch); +// this file owns the surrounding port. The three files share +// `package io;` so cross-file refs resolve via the dir-enum concat +// order (io.ww < stream.ww < types.ww — `vstream` lands before the +// reader/writer/closer aliases below). // // Deferrals (drew-signed): `handle`, `seeker`, `copier`, `strerror` -// land with fold-e (need the `handle` sum). Hare's `EOF = done` +// land with fold-e2+ (need the `handle` sum). Hare's `EOF = done` // stays as `io.eof` until the `done` singleton ships (#93). package io; +import errors; + // ref/hare/io/types.ha:29-34. RDWR is spelled `3` rather than Hare's // `READ | WRITE` because the ww parser doesn't fold expressions in // enum value positions; the value SSoT stays the same bitfield. @@ -13947,7 +14078,7 @@ export type mode = enum u8 { }; // ref/hare/io/types.ha:37-41. Hare leaves the underlying implicit; -// ww requires one. i32 matches the off type that fold-e will plug +// ww requires one. i32 matches the off type that fold-e2 will plug // into the seeker signature. export type whence = enum i32 { SET = 0, @@ -13955,23 +14086,27 @@ export type whence = enum i32 { END = 2, }; -// ref/hare/io/types.ha:11 spreads `errors::error` into the union; -// lib/errors isn't ported yet, so the union carries only the tags -// observable in this fold — `underread` (from io.ww) and the -// predeclared `nomem` (#29, type.c:72 / check.ww:78). -export type error = !(underread | nomem); +// ref/hare/io/types.ha:11 spreads `...errors::error` into the union; +// ww enumerates the tags explicitly (ken's #204-block — spread of an +// errors-side tagged into this union triggers a wrapper-vs-flatten +// layout mismatch; #199b layout-extension is the deferred fix). The +// st_read/st_write dispatchers in stream.ww return +// `errors.unsupported` when the matching vtable slot is unset, so the +// variant lands here directly. +export type error = !(errors.unsupported | underread | nomem); // ref/hare/io/types.ha:46. `eof` (not Hare's `done`) per the io.ww:8 -// rationale; lifts to `done` with #93. `*stream` forward-refs the -// pre-vtable struct in io.ww — same cross-file pattern Hare uses. -export type reader = fn(s: *stream, buf: []u8) (size | eof | error); +// rationale; lifts to `done` with #93. `vstream` forward-refs the +// `*vtable` alias in stream.ww — the dispatcher receives the vtable +// pointer directly (Hare's stream.ha:33 + types.ha:46 collapse). +export type reader = fn(s: vstream, buf: []u8) (size | eof | error); // ref/hare/io/types.ha:51. No `const` qualifier — ww has none, and // lib/io/io.ww:30 + lib/sort/sort.ww:18 drop it the same way. -export type writer = fn(s: *stream, buf: []u8) (size | error); +export type writer = fn(s: vstream, buf: []u8) (size | error); // ref/hare/io/types.ha:55. -export type closer = fn(s: *stream) (void | error); +export type closer = fn(s: vstream) (void | error); // memio — in-memory io stream. // diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 4bcc0e88..8514de57 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -13921,21 +13921,152 @@ export fn close(s: *stream) (void | closed) = { return s.close(s); }; +// stream — Hare-shaped vtable port. Project #94 fold-e1. +// +// Adds the additive vtable surface alongside lib/io/io.ww's pre-vtable +// stream struct + read/write/close wrappers (which fold-e2-eN migrate +// to vtable-backed implementations and then retire). Three exports: +// +// vtable a struct of optional fn-pointer slots — reader/writer/ +// closer per ref/hare/io/stream.ha:36-42. Hare spells the +// nullable as `nullable *T`; ww parser rejects that +// spelling (#192), so each slot is a `(*T | void)` tagged +// union (Plan-9-lean per the Nullable kept ruling; opt-in +// at optionality boundaries only). +// vstream `*vtable` alias matching Hare's `stream = *vtable` +// (ref/hare/io/stream.ha:33). Named `vstream` rather than +// `stream` because lib/io/io.ww still owns the legacy +// `stream` struct name until fold-eN collapses both. +// st_read / +// st_write / +// st_close the three dispatchers per ref/hare/io/stream.ha:44-68. +// Each `match`es the matching vtable slot, returns +// `errors.unsupported` (widened twice) when the slot is +// void, otherwise calls through the fn pointer. +// +// Fold-e1 cgen/checker dependencies (all closed at HEAD): #191 +// (N_DOT on aliased-ptr receiver — `s.reader` resolves through the +// `vstream = *vtable` alias), #198 (`is` on cross-module variant +// tag), #199 α (concrete→tagged widen via NAMED-variant nominal +// match, used by `let e: error = u;`), #200 (`as` payload-extract), +// #201 (return-of-tagged-call forwards instead of rewinds), #205 +// (tagged→tagged subset accepts NAMED-variant nominal match, used +// by `return e;` where ret type contains `error` as a direct +// variant). Two direct widens compose for the void-arm `return`: +// concrete `errors.unsupported` → tagged `error` via #199 α, then +// tagged `error` → tagged `(size | eof | error)` via #205. +// +// Deferrals (drew-signed): `seeker` lands with fold-e2 once `off` +// + `whence` plug into the signature; Hare's `?`-propagating +// `st_close` body (`c(s)?;`) collapses to a direct `return (*c)(s);` +// here because #173 (TRY-on-tagged-return both-stages broken) is +// still open — the dispatcher's surface stays Hare-shaped, only the +// internal try-prop is omitted. + +package io; + +// ref/hare/io/stream.ha:36-42. Nullable spelling per #192 (ww parser +// rejects `nullable *T`); each slot is `(*T | void)`. +export type vtable = struct { + reader: (*reader | void), + writer: (*writer | void), + closer: (*closer | void), +}; + +// ref/hare/io/stream.ha:33. `vstream` not `stream` while io.ww still +// owns the legacy `stream` struct name. +export type vstream = *vtable; + +// ref/hare/io/stream.ha:44-50. The void-arm `return e;` chains two +// direct widens: `errors.unsupported → error` via #199 α, then +// `error → (size | eof | error)` via #205. The call-arm spells the +// fn-ptr call explicitly per #193 (ww requires `(*r)(args)` rather +// than implicit `r(args)`). +export fn st_read(s: vstream, buf: []u8) (size | eof | error) = { + match (s.reader) { + case void => { + let u: errors.unsupported; + let e: error = u; + return e; + }; + case let r: *reader => return (*r)(s, buf); + }; +}; + +// ref/hare/io/stream.ha:53-59. +export fn st_write(s: vstream, buf: []u8) (size | error) = { + match (s.writer) { + case void => { + let u: errors.unsupported; + let e: error = u; + return e; + }; + case let w: *writer => return (*w)(s, buf); + }; +}; + +// ref/hare/io/stream.ha:62-68. Hare propagates the close error via +// `c(s)?;` then falls through to the void arm; ww collapses to a +// direct return because #173 (TRY-on-tagged-return) is still open. +// The surface (void | error) matches; only the internal try-prop +// is omitted. +export fn st_close(s: vstream) (void | error) = { + match (s.closer) { + case void => return void; + case let c: *closer => return (*c)(s); + }; +}; + +// errors — domain-agnostic error types. Mirrors ref/hare/errors/. +// +// Named-void tagged-union variants, so `(T | errors.invalid | ...)` +// composes with every other module's error surface at the tag level. +// Per-domain errors (io.eof, io.closed, io.underread, strconv.overflow, +// ...) live in their own modules; this module ships only the generic +// conditions Hare's errors:: exports. +// +// Subset shipped today; add more from Hare's errors/common.ha as callers +// need them. + +// A function was called with an invalid combination of arguments. +package errors; + +export type invalid = !void; + +// The user does not have permission to use this resource. +export type noaccess = !void; + +// An entry was requested which does not exist. +export type noentry = !void; + +// An attempt was made to create a resource which already exists. +export type exists = !void; + +// The requested operation is not supported. +export type unsupported = !void; + // types — error union, mode/whence enums, reader/writer/closer -// fn-type aliases. Project #94 fold-1 step (d) (drew Fold D). +// fn-type aliases. Project #94 fold-1 step (d) (drew Fold D); the +// fn-aliases re-targeted from *stream → vstream as part of fold-e1 +// (vtable port in stream.ww). // // Hare splits the io module across stream.ha + types.ha and ww does // the same: lib/io/io.ww owns the (pre-vtable) `stream`, `eof`, and -// `underread` shapes; this file owns the surrounding port. Both -// files share `package io;` so forward refs (e.g. reader → stream) -// resolve via the dir-enum concat order. +// `underread` shapes; lib/io/stream.ww owns the Hare vtable port +// (`vtable`, `vstream`, `st_read`/`st_write`/`st_close` dispatch); +// this file owns the surrounding port. The three files share +// `package io;` so cross-file refs resolve via the dir-enum concat +// order (io.ww < stream.ww < types.ww — `vstream` lands before the +// reader/writer/closer aliases below). // // Deferrals (drew-signed): `handle`, `seeker`, `copier`, `strerror` -// land with fold-e (need the `handle` sum). Hare's `EOF = done` +// land with fold-e2+ (need the `handle` sum). Hare's `EOF = done` // stays as `io.eof` until the `done` singleton ships (#93). package io; +import errors; + // ref/hare/io/types.ha:29-34. RDWR is spelled `3` rather than Hare's // `READ | WRITE` because the ww parser doesn't fold expressions in // enum value positions; the value SSoT stays the same bitfield. @@ -13947,7 +14078,7 @@ export type mode = enum u8 { }; // ref/hare/io/types.ha:37-41. Hare leaves the underlying implicit; -// ww requires one. i32 matches the off type that fold-e will plug +// ww requires one. i32 matches the off type that fold-e2 will plug // into the seeker signature. export type whence = enum i32 { SET = 0, @@ -13955,23 +14086,27 @@ export type whence = enum i32 { END = 2, }; -// ref/hare/io/types.ha:11 spreads `errors::error` into the union; -// lib/errors isn't ported yet, so the union carries only the tags -// observable in this fold — `underread` (from io.ww) and the -// predeclared `nomem` (#29, type.c:72 / check.ww:78). -export type error = !(underread | nomem); +// ref/hare/io/types.ha:11 spreads `...errors::error` into the union; +// ww enumerates the tags explicitly (ken's #204-block — spread of an +// errors-side tagged into this union triggers a wrapper-vs-flatten +// layout mismatch; #199b layout-extension is the deferred fix). The +// st_read/st_write dispatchers in stream.ww return +// `errors.unsupported` when the matching vtable slot is unset, so the +// variant lands here directly. +export type error = !(errors.unsupported | underread | nomem); // ref/hare/io/types.ha:46. `eof` (not Hare's `done`) per the io.ww:8 -// rationale; lifts to `done` with #93. `*stream` forward-refs the -// pre-vtable struct in io.ww — same cross-file pattern Hare uses. -export type reader = fn(s: *stream, buf: []u8) (size | eof | error); +// rationale; lifts to `done` with #93. `vstream` forward-refs the +// `*vtable` alias in stream.ww — the dispatcher receives the vtable +// pointer directly (Hare's stream.ha:33 + types.ha:46 collapse). +export type reader = fn(s: vstream, buf: []u8) (size | eof | error); // ref/hare/io/types.ha:51. No `const` qualifier — ww has none, and // lib/io/io.ww:30 + lib/sort/sort.ww:18 drop it the same way. -export type writer = fn(s: *stream, buf: []u8) (size | error); +export type writer = fn(s: vstream, buf: []u8) (size | error); // ref/hare/io/types.ha:55. -export type closer = fn(s: *stream) (void | error); +export type closer = fn(s: vstream) (void | error); // memio — in-memory io stream. // diff --git a/test/wcc/768_io_types_run.c b/test/wcc/768_io_types_run.c index a9904145..ccce42a9 100644 --- a/test/wcc/768_io_types_run.c +++ b/test/wcc/768_io_types_run.c @@ -1,10 +1,11 @@ /* * 768_io_types_run — project #94 fold-1 step (d) (drew Fold D) sentinel. - * Pins the new lib/io/types.ww port (ref/hare/io/types.ha) — `mode` / - * `whence` enums, `error = !(underread | nomem)` union, and the - * `reader` / `writer` / `closer` fn-type aliases. The aliases forward- - * ref `*stream` from lib/io/io.ww (Hare splits the same way: - * stream.ha + types.ha share the `io` module). + * Pins lib/io/types.ww (ref/hare/io/types.ha) — `mode` / `whence` + * enums, `error = !(errors.unsupported | underread | nomem)` union, + * and the `reader` / `writer` / `closer` fn-type aliases. Fold-e1 + * re-targeted the aliases from *stream → `vstream` (= *vtable in + * lib/io/stream.ww); the rows track. Hare splits the io module the + * same way: stream.ha + types.ha share the `io` module. * * Each row is a self-contained `import io;` program: cstage `ww build` * compiles it, we run the binary and assert the exit. Rows that hit a @@ -101,57 +102,62 @@ static const struct row rows[] = { "};\n", 2, STAGE_CS | STAGE_WW }, + /* reader/writer/closer alias rows. Fold-e1 re-targeted these aliases + * from *stream → vstream (= *vtable); the test fixtures track. The + * callee receivers are typed io.vstream and the call sites build a + * stub vtable to take its address from. The vtable values are inert + * — only the dispatch type-shape is exercised. */ { "reader_alias_param", "package main;\n" "import io;\n" - "fn dummy_read(s: *io.stream, buf: []u8) (size | io.eof | io.error) = {\n" + "fn dummy_read(s: io.vstream, buf: []u8) (size | io.eof | io.error) = {\n" " return 0: size;\n" "};\n" - "fn use_reader(r: io.reader, s: *io.stream, buf: []u8) i32 = {\n" + "fn use_reader(r: io.reader, s: io.vstream, buf: []u8) i32 = {\n" " let v = r(s, buf);\n" " if (v is size) { return 11; };\n" " return -1;\n" "};\n" "export fn main() i32 = {\n" - " let st: io.stream;\n" + " let vt: io.vtable;\n" " let buf: [4]u8;\n" " let bs: []u8 = buf[0:4];\n" - " return use_reader(dummy_read, &st, bs);\n" + " return use_reader(dummy_read, &vt, bs);\n" "};\n", 11, STAGE_CS | STAGE_WW }, { "writer_alias_param", "package main;\n" "import io;\n" - "fn dummy_write(s: *io.stream, buf: []u8) (size | io.error) = {\n" + "fn dummy_write(s: io.vstream, buf: []u8) (size | io.error) = {\n" " return buf.len: size;\n" "};\n" - "fn use_writer(w: io.writer, s: *io.stream, buf: []u8) i32 = {\n" + "fn use_writer(w: io.writer, s: io.vstream, buf: []u8) i32 = {\n" " let v = w(s, buf);\n" " if (v is size) { return 22; };\n" " return -1;\n" "};\n" "export fn main() i32 = {\n" - " let st: io.stream;\n" + " let vt: io.vtable;\n" " let buf: [7]u8;\n" " let bs: []u8 = buf[0:7];\n" - " return use_writer(dummy_write, &st, bs);\n" + " return use_writer(dummy_write, &vt, bs);\n" "};\n", 22, STAGE_CS | STAGE_WW }, { "closer_alias_param", "package main;\n" "import io;\n" - "fn dummy_close(s: *io.stream) (void | io.error) = {\n" + "fn dummy_close(s: io.vstream) (void | io.error) = {\n" " return void;\n" "};\n" - "fn use_closer(c: io.closer, s: *io.stream) i32 = {\n" + "fn use_closer(c: io.closer, s: io.vstream) i32 = {\n" " let _v = c(s);\n" " return 33;\n" "};\n" "export fn main() i32 = {\n" - " let st: io.stream;\n" - " return use_closer(dummy_close, &st);\n" + " let vt: io.vtable;\n" + " return use_closer(dummy_close, &vt);\n" "};\n", 33, STAGE_CS | STAGE_WW }, diff --git a/test/wcc/775_io_vtable_run.c b/test/wcc/775_io_vtable_run.c new file mode 100644 index 00000000..a36d5335 --- /dev/null +++ b/test/wcc/775_io_vtable_run.c @@ -0,0 +1,400 @@ +/* + * 775_io_vtable_run — project #94 fold-e1 sentinel. Pins the additive + * lib/io/stream.ww vtable port: `vtable` (the three (*T|void) slots), + * `vstream` (= *vtable), and the `st_read`/`st_write`/`st_close` + * dispatchers (ref/hare/io/stream.ha:33-68). Coexists with lib/io/io.ww's + * legacy `stream` struct + read/write/close wrappers (fold-e2+ migrates + * them to vtable-backed implementations and retires the legacy surface). + * + * Each row imports io, allocates a vtable, optionally points one slot at + * a user fn (via the `(&fn): *io.` cast — see SIBLINGS below), + * calls the matching dispatcher, and checks the exit constant. Both + * stages run; cs.s == ww.s byte-id on the rows that build (rule-10). + * + * row | what it pins + * --------------------------+-------------------------------------- + * reader_set_happy | reader slot set + st_read happy path. + * | Callee returns (7: size); probe asserts + * | `r is size && r as size == 7`. Pins + * | the call-arm `case let r: *reader => + * | return (*r)(s, buf)` end-to-end. + * reader_void_unsupported | reader=void. st_read takes the void-arm + * | (two direct widens, #199 α + #205) and + * | returns the errors.unsupported variant. + * | Probe verifies the call returns + exit + * | constant only — does NOT inspect r's + * | discriminant tag (deferred #199b: the + * | wrapped-slot tag-remap currently lands + * | the value at dst tag 0, not the `error` + * | variant index; see 774 header for the + * | parallel constraint). + * writer_set_happy | symmetric to reader_set_happy on + * | st_write; callee returns (buf.len:size). + * writer_void_unsupported | symmetric to reader_void_unsupported. + * closer_void_noop | closer=void. st_close returns plain + * | `void` (no widen — direct `return void` + * | from the dispatcher). Probe asserts + * | `r is void` — the only row that can + * | inspect the tag, because no nested + * | wrapper is involved. + * closer_set | closer slot set + st_close happy path. + * | Callee returns plain `void`; the + * | dispatcher forwards via `(*c)(s)` and + * | the return surfaces at the caller. + * branched_readers | two distinct vtable instances each with + * | a different reader fn. st_read on each + * | dispatches to the right callee (1:size + * | vs 2:size). Mirrors 774's branched- + * | callee row; catches a constant-fold + * | mistake in the dispatch path. + * + * SIBLINGS (filed inline, NOT fixed here — fold-e1 is purely additive): + * + * - #199b layout-extension (wrapped-slot tag-remap): when widening a + * NAMED wrapper-typed value (e.g. `error`) into a tagged parent + * (e.g. `(size | eof | error)`), cgen walks src VARIANTS rather + * than treating src-as-a-whole, so the dst tag lands at 0 + * regardless of which wrapper variant was set. Affects rows 2 & 4 + * (unsupported via void-arm). Probes verify runtime exit-clean, + * NOT the variant tag — matches the 774 header's exact constraint. + * + * - NEW: checker rejects bare `&fn_name` assignment to a + * `(* | void)` field. `vt.reader = &myread;` errors + * with "cannot assign *fn(vstream, []u8) (size | eof | error) to + * (*reader | void)" — the structural `*fn(...)` value is not + * accepted as the `*reader` named variant of the tagged field. + * Both stages reject. Symmetric to #178 (typeeqast layer asymmetry) + * at the pointer-to-fn-alias layer + tagged-variant assignment. + * PROBE ROUTES AROUND via an explicit cast: `(&myread): *io.reader`. + * Once closed, the cast drops out of every Hare-faithful vtable + * initialisation. + * + * - NEW: `let p: *io.reader = &myread;` also errors with "init + * *fn(vstream, []u8) ... not assignable to declared *reader" — + * same root as above but at the let-binding layer. The Hare + * `io::vtable { reader = &my_read, ... }` shape needs both holes + * closed. + * + * GATE POLARITY: must stay GREEN. A red here means the vtable port + * regressed at the checker, the dispatcher cgen miscompiled the + * call-arm, or the field-slot zero-init lost the void tag. + */ +#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[] = { + { "reader_set_happy", + "package main;\n" + "import io;\n" + "fn myread(s: io.vstream, buf: []u8) (size | io.eof | io.error) = {\n" + " return 7: size;\n" + "};\n" + "export fn main() i32 = {\n" + " let vt: io.vtable;\n" + " vt.reader = (&myread): *io.reader;\n" + " let v: io.vstream = &vt;\n" + " let buf: [4]u8;\n" + " let bs: []u8 = buf[0:4];\n" + " let r = io.st_read(v, bs);\n" + " if (r is size) {\n" + " let n = r as size;\n" + " if (n == 7: size) { return 70; };\n" + " return 71;\n" + " };\n" + " return 99;\n" + "};\n", + 70, + STAGE_CS | STAGE_WW, 1 }, + { "reader_void_unsupported", + "package main;\n" + "import io;\n" + "export fn main() i32 = {\n" + " let vt: io.vtable;\n" + " let v: io.vstream = &vt;\n" + " let buf: [4]u8;\n" + " let bs: []u8 = buf[0:4];\n" + " let _r = io.st_read(v, bs);\n" + " return 11;\n" + "};\n", + 11, + STAGE_CS | STAGE_WW, 1 }, + { "writer_set_happy", + "package main;\n" + "import io;\n" + "fn mywrite(s: io.vstream, buf: []u8) (size | io.error) = {\n" + " return buf.len: size;\n" + "};\n" + "export fn main() i32 = {\n" + " let vt: io.vtable;\n" + " vt.writer = (&mywrite): *io.writer;\n" + " let v: io.vstream = &vt;\n" + " let buf: [5]u8;\n" + " let bs: []u8 = buf[0:5];\n" + " let r = io.st_write(v, bs);\n" + " if (r is size) {\n" + " let n = r as size;\n" + " if (n == 5: size) { return 80; };\n" + " return 81;\n" + " };\n" + " return 99;\n" + "};\n", + 80, + STAGE_CS | STAGE_WW, 1 }, + { "writer_void_unsupported", + "package main;\n" + "import io;\n" + "export fn main() i32 = {\n" + " let vt: io.vtable;\n" + " let v: io.vstream = &vt;\n" + " let buf: [3]u8;\n" + " let bs: []u8 = buf[0:3];\n" + " let _r = io.st_write(v, bs);\n" + " return 22;\n" + "};\n", + 22, + STAGE_CS | STAGE_WW, 1 }, + { "closer_void_noop", + "package main;\n" + "import io;\n" + "export fn main() i32 = {\n" + " let vt: io.vtable;\n" + " let v: io.vstream = &vt;\n" + " let r = io.st_close(v);\n" + " if (r is void) { return 50; };\n" + " return 99;\n" + "};\n", + 50, + STAGE_CS | STAGE_WW, 1 }, + { "closer_set", + "package main;\n" + "import io;\n" + "fn myclose(s: io.vstream) (void | io.error) = {\n" + " return void;\n" + "};\n" + "export fn main() i32 = {\n" + " let vt: io.vtable;\n" + " vt.closer = (&myclose): *io.closer;\n" + " let v: io.vstream = &vt;\n" + " let r = io.st_close(v);\n" + " if (r is void) { return 60; };\n" + " return 99;\n" + "};\n", + 60, + STAGE_CS | STAGE_WW, 1 }, + { "branched_readers", + "package main;\n" + "import io;\n" + "fn r1(s: io.vstream, buf: []u8) (size | io.eof | io.error) = {\n" + " return 1: size;\n" + "};\n" + "fn r2(s: io.vstream, buf: []u8) (size | io.eof | io.error) = {\n" + " return 2: size;\n" + "};\n" + "export fn main() i32 = {\n" + " let vt1: io.vtable;\n" + " let vt2: io.vtable;\n" + " vt1.reader = (&r1): *io.reader;\n" + " vt2.reader = (&r2): *io.reader;\n" + " let buf: [4]u8;\n" + " let bs: []u8 = buf[0:4];\n" + " let a = io.st_read(&vt1, bs);\n" + " let b = io.st_read(&vt2, bs);\n" + " let asz: size = 0;\n" + " let bsz: size = 0;\n" + " if (a is size) { asz = a as size; };\n" + " if (b is size) { bsz = b as size; };\n" + " if (asz == 1: size && bsz == 2: size) { return 95; };\n" + " return 99;\n" + "};\n", + 95, + 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 them 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/iov_%d_d_%d", getpid(), seq); + snprintf(base, sizeof base, "main775"); + 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/iov_%d_c_%d", getpid(), seq); + snprintf(tdw, sizeof tdw, "/tmp/iov_%d_w_%d", getpid(), seq); + snprintf(base, sizeof base, "main775"); + 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, + "io_vtable_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, + "io_vtable_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, + "io_vtable_run[byte-id][%s]: cstage vs wwstage asm differs\n", + rows[i].label); + fail++; + } + } + } + } + + if (!wwpresent) + fprintf(stderr, "io_vtable_run: skip wwstage (no %s)\n", wdrv); + + if (fail) { + fprintf(stderr, "io_vtable_run: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("io_vtable_run: %d/%d ok\n", total, total); + return 0; +}