lib/io: port ref/hare/io/stream.ha vtable surface (#94 fold-e1)

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.
This commit is contained in:
2026-05-29 07:28:59 +09:00
parent 487cf91f12
commit 90ed913160
7 changed files with 856 additions and 67 deletions

95
lib/io/stream.ww Normal file
View File

@@ -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);
};
};

View File

@@ -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);