/* * 783_amp_fn_assign_run — project #206 close. Pins that a bare * `&fn_name` is assignable into a `*` slot and a * `(* | void)` tagged slot (the io vstream vtable shape) * WITHOUT the explicit `(&fn): *alias` cast that the lib/{io,memio, * fmt,bufio,log} vstream surfaces currently carry. #94 fold-eFinal * drops those casts wholesale once this is green. * * THE FIX (Option C, both stages, checker-only — cgen is a no-op * fn-pointer reinterpret): type_assignable / isassignable stay fully * NOMINAL (a materialized `*fn` value laundered into a `*alias` is * rejected, mirror of harec ref/harec/src/types.c:1039-1066). A * caller-site gate `assignable_addrfn` (cmd/wcc/check.c) / * `assignableaddrfn` (selfhost/cmd/wcc/check.ww) admits ONLY a DIRECT * `&`-of-fn-ident whose signature structurally matches the * destination's pointed-to fn alias, or — for a tagged dst — the * single matching ptr-to-fn variant (>=2 same-sig variants is * ambiguous → reject). This is harec's adopt-the-alias-at-the-`&`-site * rule (ref/harec/src/check.c:3594-3626) without threading a type * hint through the bottom-up expression checker. * * ROW POLARITY: * POSITIVE rows build + run on BOTH stages and assert cs.s == ww.s * (rule-10). `let_call` and `structlit_build` pin the let-binding and * struct-literal-field-init gate sites. `fieldstore_dispatch` pins the * eFinal SHIPPING shape: a bare &fn field-STORE into one slot of the * three-slot io.vtable (reader/writer/closer), then a dispatcher match * on a *vtable POINTER-param (test 775) — exercising both the live- * code-address call-through and the void-arm. It is byte-id-clean: the * single-slot local-composite zero-init divergence (project #213, * reproduces with the cast form and a pure `(i32|void)` field — NOT a * #206 regression) is a single-field artifact the real three-slot * io.vtable does not hit. * * NEGATIVE non-tagged rows (`neg_launder`, `neg_samesig`) MUST fail * to build on BOTH stages — #206 turned the wwstage lenient pointer-fn * punt into a confident reject, so a laundered `*fn` value and a * same-signature distinct alias are nominally rejected on both. * * NEGATIVE tagged-slot rows (`neg_ambiguous_tagged`, * `neg_launder_tagged`) are pinned CSTAGE-ONLY. cstage REJECTS both. * wwstage WRONGLY ACCEPTS them: its `(X | void)` tagged-assignability * leniently matches any `*fn` against the `void` variant (project * #214, the void-variant OVER-acceptance). #214 is an accept-INVALID * hole and its close is REQUIRED BEFORE wwstage can become the * authoritative selfhost checker; these rows graduate to STAGE_WW * when #214 closes. Mirrors the 777/780/781/782 STAGE_CS carve-out. * * GATE POLARITY: must stay GREEN. Red means the #206 gate over- or * under-accepts, the nominal reject regressed, or the fn-pointer * call-arm miscompiled. */ #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; /* expected program exit (build+run rows) */ int stage_mask; int byte_id; /* assert cs.s == ww.s */ int expect_fail; /* 1 = build MUST fail; want_exit ignored */ }; static const struct row rows[] = { /* POSITIVE byte-id: bare &fn into a * let, then call. */ { "let_call", "package main;\n" "type reader = fn(x: i32) i32;\n" "fn rd(x: i32) i32 = { return x + 1; };\n" "fn main() i32 = {\n" " let p: *reader = &rd;\n" " return (*p)(41);\n" "};\n", 42, STAGE_CS | STAGE_WW, 1, 0 }, /* POSITIVE byte-id: struct-literal field-init of a (*alias|void) * slot with bare &fn (the Hare `vtable{reader=&fn}` shape). Build * only — no match (project #212 blocks struct-lit-tagged + match * on wwstage cgen); the checker gate acceptance is what this pins. */ { "structlit_build", "package main;\n" "type reader = fn(x: i32) i32;\n" "type vtable = struct { r: (*reader | void) };\n" "fn rd(x: i32) i32 = { return x + 1; };\n" "fn main() i32 = {\n" " let v = vtable { r = &rd };\n" " return 0;\n" "};\n", 0, STAGE_CS | STAGE_WW, 1, 0 }, /* POSITIVE byte-id: bare &fn FIELD-STORE into a (*reader|void) slot * of the THREE-slot vtable (the exact io.vtable reader/writer/closer * eFinal shape, test 775), then dispatcher match on a *vtable * POINTER-param. The live-code-address call-through AND the void-arm * discrimination are exercised, and cs.s == ww.s (rule-10). This is * the eFinal SHIPPING path, so it MUST be byte-id-clean: the single- * slot local-composite zero-init divergence (#213) is a single-field * artifact that the real three-slot io.vtable does not hit. */ { "fieldstore_dispatch", "package main;\n" "type reader = fn(s: *vtable, x: i32) i32;\n" "type vtable = struct {\n" " reader: (*reader | void),\n" " writer: (*reader | void),\n" " closer: (*reader | void),\n" "};\n" "fn rd(s: *vtable, x: i32) i32 = { return x + 1; };\n" "fn dispatch(s: *vtable, x: i32) i32 = {\n" " match (s.reader) {\n" " case void => { return -1; };\n" " case let f: *reader => { return (*f)(s, x); };\n" " };\n" "};\n" "fn main() i32 = {\n" " let a: vtable;\n" " a.reader = &rd;\n" " let b: vtable;\n" " b.reader = void;\n" " return dispatch(&a, 41) + dispatch(&b, 0) + 11;\n" "};\n", 52, STAGE_CS | STAGE_WW, 1, 0 }, /* NEGATIVE both stages: a materialized *fn value laundered into a * *reader slot. harec rejects (nominal pointer assignability); both * ww stages now reject too. */ { "neg_launder", "package main;\n" "type reader = fn(x: i32) i32;\n" "fn rd(x: i32) i32 = { return x + 1; };\n" "fn main() i32 = {\n" " let p = &rd;\n" " let s: *reader = p;\n" " return 0;\n" "};\n", 0, STAGE_CS | STAGE_WW, 0, 1 }, /* NEGATIVE both stages: distinct same-signature aliases. A `&add1` * must NOT flow into a *negator slot — defeating that nominal * distinction is exactly what the gate's structural-but-direct rule * prevents (the gate fires for &add1 only against an alias whose * underlying fn matches; the laundered value here is not a direct * &fn, and even a direct &add1 into *negator is accepted only if * negator's underlying matches — which it does structurally, so the * laundering form below, NOT a direct &fn, is the one that must * reject). */ { "neg_samesig", "package main;\n" "type adder = fn(x: i32) i32;\n" "type negator = fn(x: i32) i32;\n" "fn add1(x: i32) i32 = { return x + 1; };\n" "fn main() i32 = {\n" " let p = &add1;\n" " let n: *negator = p;\n" " return 0;\n" "};\n", 0, STAGE_CS | STAGE_WW, 0, 1 }, /* NEGATIVE cstage-only (#214 wwstage void-variant over-acceptance): * two same-signature ptr-to-fn variants in the tagged dst — a direct * &fn is ambiguous and must be rejected, never silently bound to one. */ { "neg_ambiguous_tagged", "package main;\n" "type reader = fn(x: i32) i32;\n" "type writer = fn(x: i32) i32;\n" "fn myfn(x: i32) i32 = { return x; };\n" "fn main() i32 = {\n" " let x: (*reader | *writer | void) = &myfn;\n" " return 0;\n" "};\n", 0, STAGE_CS, 0, 1 }, /* NEGATIVE cstage-only (#214): laundering a materialized *fn into a * (*reader|void) tagged slot. cstage rejects nominally; wwstage's * void-variant leniency wrongly accepts. */ { "neg_launder_tagged", "package main;\n" "type reader = fn(x: i32) i32;\n" "fn rd(x: i32) i32 = { return x + 1; };\n" "fn main() i32 = {\n" " let p = &rd;\n" " let s: (*reader | void) = p;\n" " return 0;\n" "};\n", 0, STAGE_CS, 0, 1 }, }; static void cleanup_tmp(const char *tmpdir, const char *base) { char p[1024]; snprintf(p, sizeof p, "%s/%s", tmpdir, base); unlink(p); snprintf(p, sizeof p, "%s/%s.ww", 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); /* #93: 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 write_source(const char *path, const char *src) { FILE *f = fopen(path, "wb"); if (!f) return -1; fputs(src, f); fclose(f); return 0; } static int build_via_driver(const char *driver, const char *tmpdir, const char *src) { char cmd[2048]; /* #93 sep layout: `--sep -o ` relocates artifacts to * .sepwork/; WW_PKGCACHE is pinned under tmpdir so the shared * out/.pkgcache is untouched. expect_fail rows still fail: a checker * error makes w6c return nonzero regardless of artifact placement. */ snprintf(cmd, sizeof cmd, "cd %s && b='%s'; WW_PKGCACHE=%s/pkgc timeout 180 %s build --sep " "-o \"${b%%.ww}\" \"$b\" 2>/dev/null", tmpdir, src, tmpdir, driver); return runwait(cmd); } /* run_row — build via driver, run the binary, return exit (or -1 on * build failure). */ static int run_row(const char *driver, const struct row *r, int seq) { char tmpdir[256], src[512], base[64], outbin[768]; snprintf(tmpdir, sizeof tmpdir, "/tmp/afa_%d_d_%d", getpid(), seq); snprintf(base, sizeof base, "main783"); 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; if (build_via_driver(driver, tmpdir, src) == 0) { snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); rc = runwait(outbin); } else { rc = -1; } cleanup_tmp(tmpdir, base); return rc; } /* build_ok — 1 iff the build succeeds (used by expect_fail rows). */ static int build_ok(const char *driver, const struct row *r, int seq) { char tmpdir[256], src[512], base[64]; snprintf(tmpdir, sizeof tmpdir, "/tmp/afa_%d_nf_%d", getpid(), seq); snprintf(base, sizeof base, "main783"); 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 br = build_via_driver(driver, tmpdir, src); cleanup_tmp(tmpdir, base); return br == 0; } /* 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 struct row *r, int seq) { char src[512], tdc[256], tdw[256], base[64], cs[512], ws[512]; snprintf(tdc, sizeof tdc, "/tmp/afa_%d_c_%d", getpid(), seq); snprintf(tdw, sizeof tdw, "/tmp/afa_%d_w_%d", getpid(), seq); snprintf(base, sizeof base, "main783"); 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, 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, 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, seq = 0; int wwpresent = (access(wdrv, X_OK) == 0); for (int i = 0; i < n; i++) { const struct row *r = &rows[i]; if (r->stage_mask & STAGE_CS) { total++; if (r->expect_fail) { if (build_ok(cdrv, r, seq++)) { fprintf(stderr, "amp_fn_assign[cs][%s]: built but expected reject\n", r->label); fail++; } } else { int got = run_row(cdrv, r, seq++); if (got != r->want_exit) { fprintf(stderr, "amp_fn_assign[cs][%s]: exit=%d want=%d\n", r->label, got, r->want_exit); fail++; } } } if (wwpresent && (r->stage_mask & STAGE_WW)) { total++; if (r->expect_fail) { if (build_ok(wdrv, r, seq++)) { fprintf(stderr, "amp_fn_assign[ww][%s]: built but expected reject\n", r->label); fail++; } } else { int got = run_row(wdrv, r, seq++); if (got != r->want_exit) { fprintf(stderr, "amp_fn_assign[ww][%s]: exit=%d want=%d\n", r->label, got, r->want_exit); fail++; } if (r->byte_id) { total++; if (asm_byte_identical(cdrv, wdrv, r, seq++) != 0) { fprintf(stderr, "amp_fn_assign[byte-id][%s]: cstage vs wwstage asm differs\n", r->label); fail++; } } } } } if (fail) { fprintf(stderr, "amp_fn_assign: %d/%d checks failed\n", fail, total); return 1; } printf("amp_fn_assign: %d/%d ok\n", total, total); return 0; }