diff --git a/Makefile b/Makefile index bd97eb45..36321ba5 100644 --- a/Makefile +++ b/Makefile @@ -329,6 +329,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_io_handle_run \ $(BIN)/test_memio_vstream_run \ $(BIN)/test_fmt_vstream_run \ $(BIN)/test_fmt_vstream_mods_run \ @@ -707,6 +708,12 @@ $(BIN)/test_io_vtable_run: test/wcc/775_io_vtable_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_io_handle_run: test/wcc/791_io_handle_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_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 \ diff --git a/lib/io/stream.ww b/lib/io/stream.ww index 5d63e0c7..f6f62b32 100644 --- a/lib/io/stream.ww +++ b/lib/io/stream.ww @@ -31,23 +31,31 @@ package io; +// Q2 layering (ratified): the file-arm of the handle dispatchers routes +// to os.{read,write,close,lseek}; ww `os` plays Hare's `sys` role, so +// `lib/io import os` is the correct direction (os is the import floor). +import os; + // ref/hare/io/stream.ha:36-42. Nullable spelling per #192 (ww parser -// rejects `nullable *T`); each slot is `(*T | void)`. +// rejects `nullable *T`); each slot is `(*T | void)`. The `seeker` slot +// (4th) lands with io fold-2 (#5); `copier` stays deferred. export type vtable = struct { reader: (*reader | void), writer: (*writer | void), closer: (*closer | void), + seeker: (*seeker | void), }; // ref/hare/io/stream.ha:33. export type stream = *vtable; -// ref/hare/io/stream.ha:44-50. The void-arm `return e;` chains two +// ref/hare/io/stream.ha:44-51. Private stream-arm body behind the +// handle-typed [[read]] (#5). 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 read(s: stream, buf: []u8) (size | eof | error) = { +fn st_read(s: stream, buf: []u8) (size | eof | error) = { match (s.reader) { case void => { let u: errors.unsupported; @@ -58,8 +66,8 @@ export fn read(s: stream, buf: []u8) (size | eof | error) = { }; }; -// ref/hare/io/stream.ha:53-59. -export fn write(s: stream, buf: []u8) (size | error) = { +// ref/hare/io/stream.ha:53-60. +fn st_write(s: stream, buf: []u8) (size | error) = { match (s.writer) { case void => { let u: errors.unsupported; @@ -74,9 +82,104 @@ export fn write(s: stream, buf: []u8) (size | error) = { // `c(s)?;` then falls through to the void arm; ww collapses to a // direct return for the void-arm fall-through shape. The surface // (void | error) matches. -export fn close(s: stream) (void | error) = { +fn st_close(s: stream) (void | error) = { match (s.closer) { case void => return void; case let c: *closer => return (*c)(s); }; }; + +// ref/hare/io/stream.ha:70-77. Clone of st_read's slot-dispatch shape +// over the new `seeker` vtable slot. +fn st_seek(s: stream, off: off, w: whence) (off | error) = { + match (s.seeker) { + case void => { + let u: errors.unsupported; + let e: error = u; + return e; + }; + case let sk: *seeker => return (*sk)(s, off, w); + }; +}; + +// ref/hare/io/handle.ha:16-23. The handle-typed public read: the +// file-arm routes to os.read (Q2 layering), the stream-arm delegates to +// the unchanged st_read vtable dispatch above. +export fn read(h: handle, buf: []u8) (size | eof | error) = { + match (h) { + case let fd: file => { + let r: i64 = os.read(fd, buf.ptr, buf.len: u64); + if (r < 0) { + // #199b: faithful errno→io.error needs the ...errors.error spread; see lib/io/types.ww:40 + let u: errors.unsupported; + let e: error = u; + return e; + }; + if (r == 0) { + let z: eof; + return z; + }; + return r: size; + }; + case let s: stream => return st_read(s, buf); + }; +}; + +// ref/hare/io/handle.ha:27-36. +export fn write(h: handle, buf: []u8) (size | error) = { + match (h) { + case let fd: file => { + let r: i64 = os.write(fd, buf.ptr, buf.len: u64); + if (r < 0) { + // #199b: faithful errno→io.error needs the ...errors.error spread; see lib/io/types.ww:40 + let u: errors.unsupported; + let e: error = u; + return e; + }; + return r: size; + }; + case let s: stream => return st_write(s, buf); + }; +}; + +// ref/hare/io/handle.ha:44-51. Hare propagates each arm's error via `?` +// then falls through; ww returns directly (matching st_close's shape). +export fn close(h: handle) (void | error) = { + match (h) { + case let fd: file => { + let r: i32 = os.close(fd); + if (r < 0) { + // #199b: faithful errno→io.error needs the ...errors.error spread; see lib/io/types.ww:40 + let u: errors.unsupported; + let e: error = u; + return e; + }; + return void; + }; + case let s: stream => return st_close(s); + }; +}; + +// ref/hare/io/handle.ha:55-62. The io.whence → os.whence cast bridges +// the two enum copies (same SET/CUR/END values); os.lseek owns the +// syscall. +export fn seek(h: handle, off: off, w: whence) (off | error) = { + match (h) { + case let fd: file => { + let r: i64 = os.lseek(fd, off, w: os.whence); + if (r < 0) { + // #199b: faithful errno→io.error needs the ...errors.error spread; see lib/io/types.ww:40 + let u: errors.unsupported; + let e: error = u; + return e; + }; + return r: off; + }; + case let s: stream => return st_seek(s, off, w); + }; +}; + +// ref/hare/io/handle.ha:65-67. +export fn tell(h: handle) (off | error) = { + return seek(h, 0, whence.CUR); +}; diff --git a/lib/io/types.ww b/lib/io/types.ww index 7de7becc..c985bef1 100644 --- a/lib/io/types.ww +++ b/lib/io/types.ww @@ -37,6 +37,21 @@ export type whence = enum i32 { END = 2, }; +// ref/hare/io/+linux/platform_file.ha:13. Hare's `file = int`; on +// linux/x86_64 Hare's `int` is 32-bit, but ww's `int` is an 8B machine +// word, so a literal `int` here would be width-wrong for a fd. i32 is +// width-faithful to Hare's real fd width (USER-ruled 2026-05-31). +export type file = i32; + +// ref/hare/io/arch+x86_64.ha:6. ABI-compatible with POSIX off_t. +export type off = i64; + +// ref/hare/io/handle.ha:12. Hare spells it `(file | *stream)`; ww's +// `stream` is already `*vtable` (the #94 collapse absorbed Hare's +// `*stream` indirection), so the faithful ww payload is `(file | +// stream)`, NOT a literal `*stream` which would be a double-pointer. +export type handle = (file | stream); + // 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 @@ -58,3 +73,8 @@ export type writer = fn(s: stream, buf: []u8) (size | error); // ref/hare/io/types.ha:55. export type closer = fn(s: stream) (void | error); + +// ref/hare/io/types.ha:76. Hare's `fn(s: *stream, off: off, w: whence)`; +// ww's `stream` is already `*vtable` (the #94 collapse), so the param is +// `stream` directly, mirroring the reader/writer/closer aliases above. +export type seeker = fn(s: stream, off: off, w: whence) (off | error); diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 86763636..514e19a9 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -14250,23 +14250,31 @@ export type underread = !i32; package io; +// Q2 layering (ratified): the file-arm of the handle dispatchers routes +// to os.{read,write,close,lseek}; ww `os` plays Hare's `sys` role, so +// `lib/io import os` is the correct direction (os is the import floor). +import os; + // ref/hare/io/stream.ha:36-42. Nullable spelling per #192 (ww parser -// rejects `nullable *T`); each slot is `(*T | void)`. +// rejects `nullable *T`); each slot is `(*T | void)`. The `seeker` slot +// (4th) lands with io fold-2 (#5); `copier` stays deferred. export type vtable = struct { reader: (*reader | void), writer: (*writer | void), closer: (*closer | void), + seeker: (*seeker | void), }; // ref/hare/io/stream.ha:33. export type stream = *vtable; -// ref/hare/io/stream.ha:44-50. The void-arm `return e;` chains two +// ref/hare/io/stream.ha:44-51. Private stream-arm body behind the +// handle-typed [[read]] (#5). 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 read(s: stream, buf: []u8) (size | eof | error) = { +fn st_read(s: stream, buf: []u8) (size | eof | error) = { match (s.reader) { case void => { let u: errors.unsupported; @@ -14277,8 +14285,8 @@ export fn read(s: stream, buf: []u8) (size | eof | error) = { }; }; -// ref/hare/io/stream.ha:53-59. -export fn write(s: stream, buf: []u8) (size | error) = { +// ref/hare/io/stream.ha:53-60. +fn st_write(s: stream, buf: []u8) (size | error) = { match (s.writer) { case void => { let u: errors.unsupported; @@ -14293,13 +14301,108 @@ export fn write(s: stream, buf: []u8) (size | error) = { // `c(s)?;` then falls through to the void arm; ww collapses to a // direct return for the void-arm fall-through shape. The surface // (void | error) matches. -export fn close(s: stream) (void | error) = { +fn st_close(s: stream) (void | error) = { match (s.closer) { case void => return void; case let c: *closer => return (*c)(s); }; }; +// ref/hare/io/stream.ha:70-77. Clone of st_read's slot-dispatch shape +// over the new `seeker` vtable slot. +fn st_seek(s: stream, off: off, w: whence) (off | error) = { + match (s.seeker) { + case void => { + let u: errors.unsupported; + let e: error = u; + return e; + }; + case let sk: *seeker => return (*sk)(s, off, w); + }; +}; + +// ref/hare/io/handle.ha:16-23. The handle-typed public read: the +// file-arm routes to os.read (Q2 layering), the stream-arm delegates to +// the unchanged st_read vtable dispatch above. +export fn read(h: handle, buf: []u8) (size | eof | error) = { + match (h) { + case let fd: file => { + let r: i64 = os.read(fd, buf.ptr, buf.len: u64); + if (r < 0) { + // #199b: faithful errno→io.error needs the ...errors.error spread; see lib/io/types.ww:40 + let u: errors.unsupported; + let e: error = u; + return e; + }; + if (r == 0) { + let z: eof; + return z; + }; + return r: size; + }; + case let s: stream => return st_read(s, buf); + }; +}; + +// ref/hare/io/handle.ha:27-36. +export fn write(h: handle, buf: []u8) (size | error) = { + match (h) { + case let fd: file => { + let r: i64 = os.write(fd, buf.ptr, buf.len: u64); + if (r < 0) { + // #199b: faithful errno→io.error needs the ...errors.error spread; see lib/io/types.ww:40 + let u: errors.unsupported; + let e: error = u; + return e; + }; + return r: size; + }; + case let s: stream => return st_write(s, buf); + }; +}; + +// ref/hare/io/handle.ha:44-51. Hare propagates each arm's error via `?` +// then falls through; ww returns directly (matching st_close's shape). +export fn close(h: handle) (void | error) = { + match (h) { + case let fd: file => { + let r: i32 = os.close(fd); + if (r < 0) { + // #199b: faithful errno→io.error needs the ...errors.error spread; see lib/io/types.ww:40 + let u: errors.unsupported; + let e: error = u; + return e; + }; + return void; + }; + case let s: stream => return st_close(s); + }; +}; + +// ref/hare/io/handle.ha:55-62. The io.whence → os.whence cast bridges +// the two enum copies (same SET/CUR/END values); os.lseek owns the +// syscall. +export fn seek(h: handle, off: off, w: whence) (off | error) = { + match (h) { + case let fd: file => { + let r: i64 = os.lseek(fd, off, w: os.whence); + if (r < 0) { + // #199b: faithful errno→io.error needs the ...errors.error spread; see lib/io/types.ww:40 + let u: errors.unsupported; + let e: error = u; + return e; + }; + return r: off; + }; + case let s: stream => return st_seek(s, off, w); + }; +}; + +// ref/hare/io/handle.ha:65-67. +export fn tell(h: handle) (off | error) = { + return seek(h, 0, whence.CUR); +}; + // errors — domain-agnostic error types. Mirrors ref/hare/errors/. // // Named-void tagged-union variants, so `(T | errors.invalid | ...)` @@ -14475,6 +14578,21 @@ export type whence = enum i32 { END = 2, }; +// ref/hare/io/+linux/platform_file.ha:13. Hare's `file = int`; on +// linux/x86_64 Hare's `int` is 32-bit, but ww's `int` is an 8B machine +// word, so a literal `int` here would be width-wrong for a fd. i32 is +// width-faithful to Hare's real fd width (USER-ruled 2026-05-31). +export type file = i32; + +// ref/hare/io/arch+x86_64.ha:6. ABI-compatible with POSIX off_t. +export type off = i64; + +// ref/hare/io/handle.ha:12. Hare spells it `(file | *stream)`; ww's +// `stream` is already `*vtable` (the #94 collapse absorbed Hare's +// `*stream` indirection), so the faithful ww payload is `(file | +// stream)`, NOT a literal `*stream` which would be a double-pointer. +export type handle = (file | stream); + // 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 @@ -14497,6 +14615,11 @@ export type writer = fn(s: stream, buf: []u8) (size | error); // ref/hare/io/types.ha:55. export type closer = fn(s: stream) (void | error); +// ref/hare/io/types.ha:76. Hare's `fn(s: *stream, off: off, w: whence)`; +// ww's `stream` is already `*vtable` (the #94 collapse), so the param is +// `stream` directly, mirroring the reader/writer/closer aliases above. +export type seeker = fn(s: stream, off: off, w: whence) (off | error); + // memio — in-memory io stream. Project #94 fold-eFinal. // // Hare's memio:: surface, drop underscores. Two flavours behind a diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 62d3d102..1f7257af 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -14250,23 +14250,31 @@ export type underread = !i32; package io; +// Q2 layering (ratified): the file-arm of the handle dispatchers routes +// to os.{read,write,close,lseek}; ww `os` plays Hare's `sys` role, so +// `lib/io import os` is the correct direction (os is the import floor). +import os; + // ref/hare/io/stream.ha:36-42. Nullable spelling per #192 (ww parser -// rejects `nullable *T`); each slot is `(*T | void)`. +// rejects `nullable *T`); each slot is `(*T | void)`. The `seeker` slot +// (4th) lands with io fold-2 (#5); `copier` stays deferred. export type vtable = struct { reader: (*reader | void), writer: (*writer | void), closer: (*closer | void), + seeker: (*seeker | void), }; // ref/hare/io/stream.ha:33. export type stream = *vtable; -// ref/hare/io/stream.ha:44-50. The void-arm `return e;` chains two +// ref/hare/io/stream.ha:44-51. Private stream-arm body behind the +// handle-typed [[read]] (#5). 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 read(s: stream, buf: []u8) (size | eof | error) = { +fn st_read(s: stream, buf: []u8) (size | eof | error) = { match (s.reader) { case void => { let u: errors.unsupported; @@ -14277,8 +14285,8 @@ export fn read(s: stream, buf: []u8) (size | eof | error) = { }; }; -// ref/hare/io/stream.ha:53-59. -export fn write(s: stream, buf: []u8) (size | error) = { +// ref/hare/io/stream.ha:53-60. +fn st_write(s: stream, buf: []u8) (size | error) = { match (s.writer) { case void => { let u: errors.unsupported; @@ -14293,13 +14301,108 @@ export fn write(s: stream, buf: []u8) (size | error) = { // `c(s)?;` then falls through to the void arm; ww collapses to a // direct return for the void-arm fall-through shape. The surface // (void | error) matches. -export fn close(s: stream) (void | error) = { +fn st_close(s: stream) (void | error) = { match (s.closer) { case void => return void; case let c: *closer => return (*c)(s); }; }; +// ref/hare/io/stream.ha:70-77. Clone of st_read's slot-dispatch shape +// over the new `seeker` vtable slot. +fn st_seek(s: stream, off: off, w: whence) (off | error) = { + match (s.seeker) { + case void => { + let u: errors.unsupported; + let e: error = u; + return e; + }; + case let sk: *seeker => return (*sk)(s, off, w); + }; +}; + +// ref/hare/io/handle.ha:16-23. The handle-typed public read: the +// file-arm routes to os.read (Q2 layering), the stream-arm delegates to +// the unchanged st_read vtable dispatch above. +export fn read(h: handle, buf: []u8) (size | eof | error) = { + match (h) { + case let fd: file => { + let r: i64 = os.read(fd, buf.ptr, buf.len: u64); + if (r < 0) { + // #199b: faithful errno→io.error needs the ...errors.error spread; see lib/io/types.ww:40 + let u: errors.unsupported; + let e: error = u; + return e; + }; + if (r == 0) { + let z: eof; + return z; + }; + return r: size; + }; + case let s: stream => return st_read(s, buf); + }; +}; + +// ref/hare/io/handle.ha:27-36. +export fn write(h: handle, buf: []u8) (size | error) = { + match (h) { + case let fd: file => { + let r: i64 = os.write(fd, buf.ptr, buf.len: u64); + if (r < 0) { + // #199b: faithful errno→io.error needs the ...errors.error spread; see lib/io/types.ww:40 + let u: errors.unsupported; + let e: error = u; + return e; + }; + return r: size; + }; + case let s: stream => return st_write(s, buf); + }; +}; + +// ref/hare/io/handle.ha:44-51. Hare propagates each arm's error via `?` +// then falls through; ww returns directly (matching st_close's shape). +export fn close(h: handle) (void | error) = { + match (h) { + case let fd: file => { + let r: i32 = os.close(fd); + if (r < 0) { + // #199b: faithful errno→io.error needs the ...errors.error spread; see lib/io/types.ww:40 + let u: errors.unsupported; + let e: error = u; + return e; + }; + return void; + }; + case let s: stream => return st_close(s); + }; +}; + +// ref/hare/io/handle.ha:55-62. The io.whence → os.whence cast bridges +// the two enum copies (same SET/CUR/END values); os.lseek owns the +// syscall. +export fn seek(h: handle, off: off, w: whence) (off | error) = { + match (h) { + case let fd: file => { + let r: i64 = os.lseek(fd, off, w: os.whence); + if (r < 0) { + // #199b: faithful errno→io.error needs the ...errors.error spread; see lib/io/types.ww:40 + let u: errors.unsupported; + let e: error = u; + return e; + }; + return r: off; + }; + case let s: stream => return st_seek(s, off, w); + }; +}; + +// ref/hare/io/handle.ha:65-67. +export fn tell(h: handle) (off | error) = { + return seek(h, 0, whence.CUR); +}; + // errors — domain-agnostic error types. Mirrors ref/hare/errors/. // // Named-void tagged-union variants, so `(T | errors.invalid | ...)` @@ -14475,6 +14578,21 @@ export type whence = enum i32 { END = 2, }; +// ref/hare/io/+linux/platform_file.ha:13. Hare's `file = int`; on +// linux/x86_64 Hare's `int` is 32-bit, but ww's `int` is an 8B machine +// word, so a literal `int` here would be width-wrong for a fd. i32 is +// width-faithful to Hare's real fd width (USER-ruled 2026-05-31). +export type file = i32; + +// ref/hare/io/arch+x86_64.ha:6. ABI-compatible with POSIX off_t. +export type off = i64; + +// ref/hare/io/handle.ha:12. Hare spells it `(file | *stream)`; ww's +// `stream` is already `*vtable` (the #94 collapse absorbed Hare's +// `*stream` indirection), so the faithful ww payload is `(file | +// stream)`, NOT a literal `*stream` which would be a double-pointer. +export type handle = (file | stream); + // 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 @@ -14497,6 +14615,11 @@ export type writer = fn(s: stream, buf: []u8) (size | error); // ref/hare/io/types.ha:55. export type closer = fn(s: stream) (void | error); +// ref/hare/io/types.ha:76. Hare's `fn(s: *stream, off: off, w: whence)`; +// ww's `stream` is already `*vtable` (the #94 collapse), so the param is +// `stream` directly, mirroring the reader/writer/closer aliases above. +export type seeker = fn(s: stream, off: off, w: whence) (off | error); + // memio — in-memory io stream. Project #94 fold-eFinal. // // Hare's memio:: surface, drop underscores. Two flavours behind a diff --git a/test/wcc/791_io_handle_run.c b/test/wcc/791_io_handle_run.c new file mode 100644 index 00000000..b6584f7c --- /dev/null +++ b/test/wcc/791_io_handle_run.c @@ -0,0 +1,378 @@ +/* + * 791_io_handle_run — project #5 commit-1 sentinel. Pins the lib/io + * handle dispatch surface: `handle = (file | stream)` and the + * read/write/close/seek/tell dispatchers (ref/hare/io/handle.ha:16-67) + * over BOTH arms: + * + * - file-arm: a real OS fd (io.file = i32) routed to + * os.{read,write,lseek,close} — the Q2 layering (lib/io imports os, + * os is the import floor). The file-arm syscall-error path returns + * the #199b errors.unsupported placeholder (see lib/io/types.ww:40), + * pinned by the file_arm_error_stub row (a never-opened fd forces + * EBADF on every file-arm dispatcher). + * - stream-arm: a memio (or hand-built) vtable routed to the private + * st_read/st_write/st_seek (ref/hare/io/stream.ha:44-77). + * + * row | what it pins + * -------------------------+-------------------------------------- + * file_arm_roundtrip | open(/tmp) → io.write "ABC" → io.seek + * | SET 0 → io.read back → io.seek END == 3 + * | → io.tell == 3 → io.close. Real fd + * | through every file-arm dispatcher; + * | SET+END+CUR(via tell) whence. exit 42. + * stream_write_read | memio.fixed: io.write 3 bytes, verify + * | the backing buffer, then read them back + * | through io.read on a fresh fixed stream. + * | exit 43. + * stream_seek_unsupported | memio wires no seeker slot; io.seek + * | takes st_seek's void-arm and returns an + * | io.error (not an off). exit 44. + * stream_seeker_tell | hand-built vtable WITH a seeker fn: + * | io.seek returns the seeker's off, and + * | io.tell == seek(s, 0, CUR). exit 45. + * file_arm_error_stub | never-opened fd (9999) → every file-arm + * | dispatcher's syscall returns -1 (EBADF) + * | → the #199b errors.unsupported stub + * | comes back as io.error. exit 46. + * + * Both stages run each row; cs.s == ww.s byte-id is asserted per row + * (rule-10). GATE POLARITY: must stay GREEN — a red means the handle + * union decompose, a dispatcher arm, or the seeker slot regressed. + */ +#include +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return -1; +} + +#define STAGE_CS 1 +#define STAGE_WW 2 + +struct row { + const char *label; + const char *src; + int want_exit; + int stage_mask; + int byte_id; +}; + +static const struct row rows[] = { + { "file_arm_roundtrip", + "package main;\n" + "import io;\n" + "import os;\n" + "export fn main() i32 = {\n" + " let cflags: os.flag = os.flag.RDWR | os.flag.CREATE | os.flag.TRUNC;\n" + " let fd: i32 = 0;\n" + " match (os.tryopen(\"/tmp/ww_io791_handle.dat\", cflags, 420i32)) {\n" + " case let e: os.oserror => return 81;\n" + " case let f: i32 => fd = f;\n" + " };\n" + " let h: io.file = fd;\n" + " let data: [3]u8;\n" + " data[0] = 65u8; data[1] = 66u8; data[2] = 67u8;\n" + " match (io.write(h, data[0:3])) {\n" + " case let n: size => { if (n != 3: size) { return 82; }; };\n" + " case let e: io.error => return 83;\n" + " };\n" + " match (io.seek(h, 0: io.off, io.whence.SET)) {\n" + " case let o: io.off => { if (o != 0: io.off) { return 84; }; };\n" + " case let e: io.error => return 85;\n" + " };\n" + " let rb: [3]u8;\n" + " match (io.read(h, rb[0:3])) {\n" + " case let n: size => { if (n != 3: size) { return 86; }; };\n" + " case let z: io.eof => return 87;\n" + " case let e: io.error => return 88;\n" + " };\n" + " if (rb[0] != 65u8 || rb[1] != 66u8 || rb[2] != 67u8) { return 89; };\n" + " match (io.seek(h, 0: io.off, io.whence.END)) {\n" + " case let o: io.off => { if (o != 3: io.off) { return 93; }; };\n" + " case let e: io.error => return 94;\n" + " };\n" + " match (io.tell(h)) {\n" + " case let o: io.off => { if (o != 3: io.off) { return 90; }; };\n" + " case let e: io.error => return 91;\n" + " };\n" + " match (io.close(h)) {\n" + " case void => void;\n" + " case let e: io.error => return 92;\n" + " };\n" + " return 42;\n" + "};\n", + 42, + STAGE_CS | STAGE_WW, 1 }, + { "stream_write_read", + "package main;\n" + "import io;\n" + "import memio;\n" + "export fn main() i32 = {\n" + " let buf: [8]u8;\n" + " let st: memio.stream = memio.fixed(buf[0:8]);\n" + " let s: io.stream = &st.vt;\n" + " let data: [3]u8;\n" + " data[0] = 88u8; data[1] = 89u8; data[2] = 90u8;\n" + " match (io.write(s, data[0:3])) {\n" + " case let n: size => { if (n != 3: size) { return 71; }; };\n" + " case let e: io.error => return 72;\n" + " };\n" + " if (buf[0] != 88u8 || buf[1] != 89u8 || buf[2] != 90u8) { return 73; };\n" + " let st2: memio.stream = memio.fixed(buf[0:8]);\n" + " let s2: io.stream = &st2.vt;\n" + " let rb: [3]u8;\n" + " match (io.read(s2, rb[0:3])) {\n" + " case let n: size => { if (n != 3: size) { return 74; }; };\n" + " case let z: io.eof => return 75;\n" + " case let e: io.error => return 76;\n" + " };\n" + " if (rb[0] != 88u8 || rb[1] != 89u8 || rb[2] != 90u8) { return 77; };\n" + " return 43;\n" + "};\n", + 43, + STAGE_CS | STAGE_WW, 1 }, + { "stream_seek_unsupported", + "package main;\n" + "import io;\n" + "import memio;\n" + "export fn main() i32 = {\n" + " let buf: [4]u8;\n" + " let st: memio.stream = memio.fixed(buf[0:4]);\n" + " let s: io.stream = &st.vt;\n" + " match (io.seek(s, 0: io.off, io.whence.SET)) {\n" + " case let o: io.off => return 61;\n" + " case let e: io.error => return 44;\n" + " };\n" + "};\n", + 44, + STAGE_CS | STAGE_WW, 1 }, + { "stream_seeker_tell", + "package main;\n" + "import io;\n" + "fn myseek(s: io.stream, off: io.off, w: io.whence) (io.off | io.error) = {\n" + " return off + 100: io.off;\n" + "};\n" + "export fn main() i32 = {\n" + " let vt: io.vtable;\n" + " vt.seeker = (&myseek): *io.seeker;\n" + " let s: io.stream = &vt;\n" + " match (io.seek(s, 5: io.off, io.whence.SET)) {\n" + " case let o: io.off => { if (o != 105: io.off) { return 51; }; };\n" + " case let e: io.error => return 52;\n" + " };\n" + " match (io.tell(s)) {\n" + " case let o: io.off => { if (o != 100: io.off) { return 53; }; };\n" + " case let e: io.error => return 54;\n" + " };\n" + " return 45;\n" + "};\n", + 45, + STAGE_CS | STAGE_WW, 1 }, + { "file_arm_error_stub", + "package main;\n" + "import io;\n" + "export fn main() i32 = {\n" + /* A never-opened fd: every file-arm syscall returns -1 (EBADF), + * driving the #199b errors.unsupported stub on all four + * dispatchers (write/read/seek/close). Since that stub is the + * only error-producing path reachable here, an io.error arm + * taken == the placeholder came back. */ + " let h: io.file = 9999i32;\n" + " let data: [3]u8;\n" + " data[0] = 65u8; data[1] = 66u8; data[2] = 67u8;\n" + " match (io.write(h, data[0:3])) {\n" + " case let n: size => return 61;\n" + " case let e: io.error => void;\n" + " };\n" + " match (io.read(h, data[0:3])) {\n" + " case let n: size => return 62;\n" + " case let z: io.eof => return 63;\n" + " case let e: io.error => void;\n" + " };\n" + " match (io.seek(h, 0: io.off, io.whence.SET)) {\n" + " case let o: io.off => return 64;\n" + " case let e: io.error => void;\n" + " };\n" + " match (io.close(h)) {\n" + " case void => return 65;\n" + " case let e: io.error => void;\n" + " };\n" + " return 46;\n" + "};\n", + 46, + 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/ioh_%d_d_%d", getpid(), seq); + snprintf(base, sizeof base, "main791"); + 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/ioh_%d_c_%d", getpid(), seq); + snprintf(tdw, sizeof tdw, "/tmp/ioh_%d_w_%d", getpid(), seq); + snprintf(base, sizeof base, "main791"); + 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_handle_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_handle_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_handle_run[byte-id][%s]: cstage vs wwstage asm differs\n", + rows[i].label); + fail++; + } + } + } + } + + if (!wwpresent) + fprintf(stderr, "io_handle_run: skip wwstage (no %s)\n", wdrv); + + if (fail) { + fprintf(stderr, "io_handle_run: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("io_handle_run: %d/%d ok\n", total, total); + return 0; +}