Files
ww/test/wcc/783_amp_fn_assign_run.c
Hojun-Cho eb6083e54a test: retarget Pattern-B gates to sep .sepwork layout; 915 off private global (M4 E3, #93)
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).
2026-06-18 11:16:56 +09:00

398 lines
14 KiB
C

/*
* 783_amp_fn_assign_run — project #206 close. Pins that a bare
* `&fn_name` is assignable into a `*<fn-alias>` slot and a
* `(*<fn-alias> | 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 <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; /* 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 *<fn-alias> 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 <src-stem>` relocates artifacts to
* <stem>.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;
}