Files
ww/test/wcc/775_io_vtable_run.c
Hojun-Cho 7d39f6d623 lib: collapse the parallel vstream scaffold onto the single Hare io surface (#94 fold-eFinal)
The Option-C parallel _v vstream API was scaffolding to bring the io stack up alongside the old surface; carrying both permanently is a rule-9 divergence from ref/hare, which has exactly one io surface. Collapse onto that surface (stream = *vtable, ref/hare/io/stream.ha) and rename the _v symbols to their Hare names (io vstream->stream, fmt vfprint->fprint, bufio/memio/log surfaces, log.new). Deletes the 4 lib/*/vstream.ww scaffold files; regenerates w6c/wwdump combined.ww. cstage and wwstage stay byte-identical and combined_ww_fresh holds; all 220 tests pass.
2026-05-30 03:24:23 +09:00

403 lines
13 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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.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/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.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_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;
}