wwstage exprtype's N_CALL arm fell into a global-leaf scopelookup for an N_DOT callee with a value or chained receiver, binding whatever same-named global headed the scope bucket. Under a late-os combined.ww concat order this resolved io's `s.read(...)` to os.read (i64) instead of the field's fn type, so checkretassign confidently rejected a valid tagged return — an import-order-sensitive false positive. cstage resolves a call result solely from the callee expr's own type (check.c:1378-1433, mirroring harec check_autodereference 1566-1581); drop the global-leaf else-arm so value and chained receivers fall through to the existing fn-VALUE path at check.ww:2455. SK_USE module-qualified calls are unchanged. ww has no methods, so `value.leaf()` is only ever a fn-ptr field access; the global hit was never legitimate. Zero .s delta across all 5 bootstrap tools (the branch is dead in the bootstrap); test 776 graduates to both stages (os-late order, byte-identical). The fix unmasks a pre-existing wwstage cgen bug (#211): cgen also re-derives a call's return shape by name (fnretlookup), so a value-receiver field call whose leaf collides with a same-named global of a different register shape mis-resolves cs!=ww. Documented at the cgen site; pinned cstage-only by test/wcc/782 (graduates to STAGE_WW on #211 close).
386 lines
13 KiB
C
386 lines
13 KiB
C
/*
|
|
* 776_memio_vstream_run — project #94 fold-e2 sentinel. Pins the
|
|
* additive lib/memio/vstream.ww Option C parallel API: heap-alloc'd
|
|
* ctx structs (fixed_ctx / dynamic_ctx) with `vt: io.vtable` as the
|
|
* first field for the intrusive cast, three constructors
|
|
* (fixed_vstream / dynamic_vstream / dynamicfrom_vstream) that
|
|
* return `(io.vstream | nomem)`, and the matching read/write/close
|
|
* callbacks that recover the outer ctx via a vstream→*ctx pointer
|
|
* cast. Coexists with the pre-vtable memio.fixed/dynamic/dynamicfrom
|
|
* surface in memio.ww (fold-eFinal, task #50, retires the latter).
|
|
*
|
|
* Each row imports memio + io, calls one or more constructors, drives
|
|
* the io.st_read/st_write/st_close dispatchers, and asserts an exit
|
|
* constant. Both stages run; cs.s == ww.s byte-id on every row.
|
|
*
|
|
* row | what it pins
|
|
* --------------------------+--------------------------------------
|
|
* fixed_read_5 | fixed_vstream + io.st_read of 5 bytes
|
|
* | over an 8-byte source. Asserts the
|
|
* | return is `size` and equal to 5, and
|
|
* | that out[0]/out[4] mirror the buffer.
|
|
* | Pins the full intrusive shape (alloc
|
|
* | fixed_ctx → return &c.vt; dispatcher
|
|
* | recovers via s: *fixed_ctx).
|
|
* dynamic_write_grow | dynamic_vstream + io.st_write of 3
|
|
* | bytes. Asserts the write reports 3
|
|
* | and that the dynamicgrow_v path
|
|
* | allocated the backing buffer (initial
|
|
* | cap=0 forces growth on the first
|
|
* | write). Closes via io.st_close to
|
|
* | exercise dynamicclose_v's free.
|
|
* dynamicfrom_alt_rw | dynamicfrom_vstream seeded with a
|
|
* | 4-byte slice. Alternates st_read
|
|
* | (4 bytes back) → st_write (2 bytes,
|
|
* | grows past cap=4 → realloc). Asserts
|
|
* | both paths report their lengths.
|
|
* | Single ctx flavour, two dispatcher
|
|
* | paths through the same vtable.
|
|
* branched_fixed | branched callee per #105: two
|
|
* | fixed_vstream instances with
|
|
* | different buffer sizes (3 and 5),
|
|
* | runtime-selected by the exit code
|
|
* | nudge. st_read on each returns the
|
|
* | matching size. Catches a constant-
|
|
* | fold mistake in the dispatch path
|
|
* | (mirror of 775's branched_readers
|
|
* | row for the vtable-init side).
|
|
*
|
|
* IMPORT ORDER (#208 CLOSED): every row imports `memio; io; os;` —
|
|
* os LAST. This is the formerly-failing os-late ordering. Pre-#208 it
|
|
* tripped the wwstage checker on io.read/write/close's `return s.read(
|
|
* s, buf)` (a fn-pointer field call) with 3 false "return: not
|
|
* assignable (i64 → )"/"(i32 → )" — because exprtype re-bound the field
|
|
* leaf to a same-named scalar global (os.read:i64) instead of the
|
|
* field's tagged fn type. #208 fixed exprtype to resolve value-receiver
|
|
* field calls from the field type (cmd/wcc/check.c:1378-1433 twin), so
|
|
* the order no longer matters and all 4 rows now run STAGE_CS|STAGE_WW
|
|
* byte-id. The earlier `import os;`-FIRST workaround is retired here.
|
|
*
|
|
* SIBLINGS (filed inline, NOT fixed here — fold-e2 is purely
|
|
* additive over fold-e1's frozen io.* surface):
|
|
*
|
|
* - #173 (TRY-on-tagged-return both-stages broken) blocks
|
|
* fixed_write_v from returning Hare's `nomem` when full;
|
|
* vstream.ww surfaces 0 instead (memio.ww:155 OLD divergence).
|
|
* The probe doesn't pin Hare-equivalent nomem behaviour;
|
|
* fold-eFinal can graduate once #173 closes.
|
|
*
|
|
* - #195 (cgassign N_DOT TK_ASSIGN on aliased-ptr receiver):
|
|
* would bite if vstream.ww used chained `c.vt.reader = …`
|
|
* stores through the alloc'd ctx pointer. We sidestep via a
|
|
* local `let vt: io.vtable;` + field-assigns + struct-lit
|
|
* `alloc(fixed_ctx{vt=vt, …})`. Mirrors memio.ww:39's flat-
|
|
* slice-fields workaround. eFinal can collapse once #195 closes.
|
|
*
|
|
* GATE POLARITY: must stay GREEN. A red here means vstream.ww
|
|
* regressed, the alloc-struct-? path miscompiled, the intrusive
|
|
* vstream→*ctx cast miscomputed offsets, or io.vtable's first-field
|
|
* embed lost its offset-0 invariant.
|
|
*/
|
|
#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[] = {
|
|
{ "fixed_read_5",
|
|
"package main;\n"
|
|
"import memio;\n"
|
|
"import io;\n"
|
|
"import os;\n"
|
|
"export fn main() i32 = {\n"
|
|
" let buf: [8]u8;\n"
|
|
" buf[0] = 65u8; buf[1] = 66u8; buf[2] = 67u8; buf[3] = 68u8;\n"
|
|
" buf[4] = 69u8; buf[5] = 70u8; buf[6] = 71u8; buf[7] = 72u8;\n"
|
|
" let r = memio.fixed_vstream(buf[0:8]);\n"
|
|
" let s: io.vstream = nil: *io.vtable;\n"
|
|
" match (r) {\n"
|
|
" case let v: io.vstream => { s = v; };\n"
|
|
" case nomem => { return 50; };\n"
|
|
" };\n"
|
|
" let out: [5]u8;\n"
|
|
" let n: size = 0;\n"
|
|
" let rd = io.st_read(s, out[0:5]);\n"
|
|
" if (rd is size) { n = rd as size; };\n"
|
|
" if (n == 5: size && out[0] == 65u8 && out[4] == 69u8) { return 42; };\n"
|
|
" return 99;\n"
|
|
"};\n",
|
|
42,
|
|
STAGE_CS | STAGE_WW, 1 },
|
|
{ "dynamic_write_grow",
|
|
"package main;\n"
|
|
"import memio;\n"
|
|
"import io;\n"
|
|
"import os;\n"
|
|
"export fn main() i32 = {\n"
|
|
" let r = memio.dynamic_vstream();\n"
|
|
" let s: io.vstream = nil: *io.vtable;\n"
|
|
" match (r) {\n"
|
|
" case let v: io.vstream => { s = v; };\n"
|
|
" case nomem => { return 50; };\n"
|
|
" };\n"
|
|
" let src: [3]u8;\n"
|
|
" src[0] = 88u8; src[1] = 89u8; src[2] = 90u8;\n"
|
|
" let wr = io.st_write(s, src[0:3]);\n"
|
|
" let n: size = 0;\n"
|
|
" if (wr is size) { n = wr as size; };\n"
|
|
" let cl = io.st_close(s);\n"
|
|
" if (cl is void && n == 3: size) { return 43; };\n"
|
|
" return 99;\n"
|
|
"};\n",
|
|
43,
|
|
STAGE_CS | STAGE_WW, 1 },
|
|
{ "dynamicfrom_alt_rw",
|
|
"package main;\n"
|
|
"import memio;\n"
|
|
"import io;\n"
|
|
"import os;\n"
|
|
"export fn main() i32 = {\n"
|
|
" let seed: [4]u8;\n"
|
|
" seed[0] = 1u8; seed[1] = 2u8; seed[2] = 3u8; seed[3] = 4u8;\n"
|
|
" let r = memio.dynamicfrom_vstream(seed[0:4]);\n"
|
|
" let s: io.vstream = nil: *io.vtable;\n"
|
|
" match (r) {\n"
|
|
" case let v: io.vstream => { s = v; };\n"
|
|
" case nomem => { return 50; };\n"
|
|
" };\n"
|
|
" let out: [4]u8;\n"
|
|
" let rd = io.st_read(s, out[0:4]);\n"
|
|
" let rn: size = 0;\n"
|
|
" if (rd is size) { rn = rd as size; };\n"
|
|
" let extra: [2]u8;\n"
|
|
" extra[0] = 99u8; extra[1] = 100u8;\n"
|
|
" let wr = io.st_write(s, extra[0:2]);\n"
|
|
" let wn: size = 0;\n"
|
|
" if (wr is size) { wn = wr as size; };\n"
|
|
" if (rn == 4: size && wn == 2: size && out[0] == 1u8 && out[3] == 4u8) { return 44; };\n"
|
|
" return 99;\n"
|
|
"};\n",
|
|
44,
|
|
STAGE_CS | STAGE_WW, 1 },
|
|
{ "branched_fixed",
|
|
"package main;\n"
|
|
"import memio;\n"
|
|
"import io;\n"
|
|
"import os;\n"
|
|
"export fn main() i32 = {\n"
|
|
" let a: [3]u8;\n"
|
|
" a[0] = 10u8; a[1] = 11u8; a[2] = 12u8;\n"
|
|
" let b: [5]u8;\n"
|
|
" b[0] = 20u8; b[1] = 21u8; b[2] = 22u8; b[3] = 23u8; b[4] = 24u8;\n"
|
|
" let ra = memio.fixed_vstream(a[0:3]);\n"
|
|
" let rb = memio.fixed_vstream(b[0:5]);\n"
|
|
" let sa: io.vstream = nil: *io.vtable;\n"
|
|
" let sb: io.vstream = nil: *io.vtable;\n"
|
|
" match (ra) {\n"
|
|
" case let v: io.vstream => { sa = v; };\n"
|
|
" case nomem => { return 50; };\n"
|
|
" };\n"
|
|
" match (rb) {\n"
|
|
" case let v: io.vstream => { sb = v; };\n"
|
|
" case nomem => { return 51; };\n"
|
|
" };\n"
|
|
" let outa: [3]u8;\n"
|
|
" let outb: [5]u8;\n"
|
|
" let rda = io.st_read(sa, outa[0:3]);\n"
|
|
" let rdb = io.st_read(sb, outb[0:5]);\n"
|
|
" let na: size = 0;\n"
|
|
" let nb: size = 0;\n"
|
|
" if (rda is size) { na = rda as size; };\n"
|
|
" if (rdb is size) { nb = rdb as size; };\n"
|
|
" if (na == 3: size && nb == 5: size && outa[2] == 12u8 && outb[4] == 24u8) { return 45; };\n"
|
|
" return 99;\n"
|
|
"};\n",
|
|
45,
|
|
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 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/mvs_%d_d_%d", getpid(), seq);
|
|
snprintf(base, sizeof base, "main776");
|
|
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/mvs_%d_c_%d", getpid(), seq);
|
|
snprintf(tdw, sizeof tdw, "/tmp/mvs_%d_w_%d", getpid(), seq);
|
|
snprintf(base, sizeof base, "main776");
|
|
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,
|
|
"memio_vstream_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,
|
|
"memio_vstream_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,
|
|
"memio_vstream_run[byte-id][%s]: cstage vs wwstage asm differs\n",
|
|
rows[i].label);
|
|
fail++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!wwpresent)
|
|
fprintf(stderr, "memio_vstream_run: skip wwstage (no %s)\n", wdrv);
|
|
|
|
if (fail) {
|
|
fprintf(stderr, "memio_vstream_run: %d/%d fixtures failed\n",
|
|
fail, total);
|
|
return 1;
|
|
}
|
|
printf("memio_vstream_run: %d/%d ok\n", total, total);
|
|
return 0;
|
|
}
|