Files
ww/test/wcc/791_io_handle_run.c
Hojun-Cho 532b0a88ae lib/memio: wire seeker (io.seek dispatch landed; #5-era deferral stale)
io.error grows errors.invalid (ref/hare/io/types.ha:11 spreads
...errors::error, which includes it; Hare's memio seek returns it on
out-of-bounds, stream.ha:134-136) — appended last so existing member
tags stay put; no exhaustive io.error matches exist in lib.

seekfn mirrors ref/hare/memio/stream.ha:122-140 over the flat header,
shared by fixed/dynamic/dynamicfrom (Hare wires the same seek into
both vtables). The io.off-vs-i64 arithmetic runs on an i64 copy:
cstage binop typing is nominal on aliases, wwstage accepts (filed,
ww-core #54).

791's stream_seek_unsupported row re-pins st_seek's void-arm on a
hand-built seekerless vtable: its old premise (memio wires no seeker)
is retired by this commit; memio seek success is pinned by memiotest.

w6c/wwdump main.combined.ww regen'd: they embed lib/io + lib/memio
(the freshness gates are blind to this — embedded-source discipline);
w6a/w6l/ww don't embed io, verified untouched.
2026-06-04 16:10:34 +09:00

381 lines
13 KiB
C

/*
* 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 | hand-built vtable with the seeker slot
* | left void; io.seek takes st_seek's
* | void-arm and returns an io.error (not
* | an off). exit 44. (Was memio.fixed:
* | memio wires a seeker now, so its seek
* | succeeds — pinned by memiotest; this
* | row keeps the void-arm dispatch pin.)
* 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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
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"
"export fn main() i32 = {\n"
" let vt: io.vtable;\n"
" let s: io.stream = &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 <src>.{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;
}