/* * 775_io_vtable_run — project #94 fold-eFinal sentinel. Pins the * single-surface lib/io/stream.ww vtable port: `vtable` (the three * (*T|void) slots), `stream` (= *vtable), and the read/write/close * dispatchers (ref/hare/io/stream.ha:33-68). The fold-eFinal FLIP * retired the pre-vtable `stream` struct + `closed` tag, leaving this * as the sole io surface. (Comment prose below predates the FLIP and * still says `vstream`/`st_read`/`st_write`/`st_close` for the same * `stream`/`read`/`write`/`close`.) * * 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. * * DEFERRALS / RELATED (NOT addressed by these rows): * * - #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.stream, 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.stream = &vt;\n" " let buf: [4]u8;\n" " let bs: []u8 = buf[0:4];\n" " let r = io.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.stream = &vt;\n" " let buf: [4]u8;\n" " let bs: []u8 = buf[0:4];\n" " let _r = io.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.stream, 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.stream = &vt;\n" " let buf: [5]u8;\n" " let bs: []u8 = buf[0:5];\n" " let r = io.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.stream = &vt;\n" " let buf: [3]u8;\n" " let bs: []u8 = buf[0:3];\n" " let _r = io.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.stream = &vt;\n" " let r = io.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.stream) (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.stream = &vt;\n" " let r = io.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.stream, buf: []u8) (size | io.eof | io.error) = {\n" " return 1: size;\n" "};\n" "fn r2(s: io.stream, 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.read(&vt1, bs);\n" " let b = io.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", 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/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.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_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; }