/* * 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 #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" "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 .{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", tmpdir, base); unlink(p); /* #93 sep layout: the sep scratch dir + the tmpdir-pinned pkgcache. */ snprintf(p, sizeof p, "rm -rf %s/%s.sepwork %s/pkgc", tmpdir, base, tmpdir); if (system(p)) {} /* best-effort */ rmdir(tmpdir); } static int build_via_driver(const char *driver, const char *tmpdir, const char *cwd, const char *src) { char cmd[2048]; /* #93 sep layout: `--sep -o ` emits the asm to * .sepwork/__root.s; WW_PKGCACHE is pinned under tmpdir so the * shared out/.pkgcache is untouched and the cache dies with the * tmpdir. PRESERVES the `-I %s/lib` import-dir flag. */ snprintf(cmd, sizeof cmd, "cd %s && b='%s'; WW_PKGCACHE=%s/pkgc timeout 180 %s build --sep " "-I %s/lib -o \"${b%%.ww}\" \"$b\" 2>/dev/null", tmpdir, src, tmpdir, driver, cwd); 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.sepwork/__root.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.sepwork/__root.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; }