The driver flip moves build artifacts from next-to-source <stem>.s to a .sepwork/ scratch dir. 29 Pattern-B gates now build `ww build --sep -o <stem>` and read <stem>.sepwork/__root.s (multi-package gates concat all <stem>.sepwork/*.s, since cross-package labels live in per-package .s). All intermediates redirect to /tmp (WW_PKGCACHE + -o), so the corpus runs parallel-safe with no source-tree pollution. Tests pass now (--sep is live) and survive the flip. 915 additionally retargeted off strconv's PRIVATE left_shift_table (a let, not export) — which separate compilation correctly hides — onto a test-local package that exports its own probe table (#96). The combined path only linked it via a single-unit private leak; encapsulation is now honored under sep. Test-only; all 5 *_ww binaries HOLD. 989_m1mangle_run deferred (blocked by #99, imported-package fn main mangling under sep).
408 lines
14 KiB
C
408 lines
14 KiB
C
/*
|
||
* 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.<role>` 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
|
||
* `(*<alias-to-fn> | 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 <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[] = {
|
||
{ "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 <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", 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 <src-stem>` emits the asm to
|
||
* <stem>.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;
|
||
}
|