Files
ww/test/wcc/766_star_fn_deref_call.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

296 lines
9.3 KiB
C

/*
* 766_star_fn_deref_call — root-cause lock for project #181, and the
* live verification of the full deref-call c-cluster (#180 + #185 +
* #181 working together end-to-end).
*
* Pre-#181 wwstage's checker bailed asserttyped on the N_CALL whose
* callee was N_UN TK_STAR over a *fn — selfhost/cmd/wcc/check.ww
* exprtype's N_CALL arm only resolved IDENT/DOT-named callees and
* early-returned nil for any other shape, leaving e.type_ unstamped
* so the post-checker invariant gate fired. With cgen post-#180+#185
* already correct for the deref-call lowering, the safe fix was to
* lift the asserttyped bail and route the callee through the
* fn-VALUE fallback (autodereference + dealias to TY_FN, mirroring
* harec check_autodereference at ref/harec/src/check.c:1566).
*
* Fix: selfhost/cmd/wcc/check.ww exprtype N_CALL arm — replace the
* `if (nm.len == 0) return nil` early-bail with `if (nm.len > 0)
* { name-lookup }`, so non-named callees fall through to the
* existing fn-VALUE branch (peel TPTR, dealias to TFN, stamp the
* result type). cstage check.c already worked because cexpr
* recurses on the callee — TK_STAR's unop arm returns t->sub which
* is TY_FN directly, no name path needed.
*
* Gate flip from 765: 765 was cstage-only because wwstage bailed
* before reaching cgen. Post-#181 every row gates BOTH stages —
* cstage runtime, wwstage runtime, AND cs.s == ww.s byte-id. This
* is the runtime coverage 765 deferred plus the symmetry gate that
* proves both stages emit identical asm for the deref-call shape.
*
* Coverage (same 5 rows as 765, now with STAGE_CS|STAGE_WW):
* 1. minimal — `let f = &add1; (*f)(7) == 8`
* 2. branched_callee — pick aa or bb by runtime cond
* 3. alias_chain — `let f = &fn; let g = f; (*g)(7)`
* 4. fn_with_args — multiple args, scalar + ptr mix
* 5. fn_tuple_return — (i32, i32) return shape; multi-reg
* return ABI survives the deref-call
*
* Gates per row:
* a. cstage builds + runs, exit == expected (already worked
* post-#180+#185; kept for c-cluster regression coverage).
* b. wwstage builds + runs, exit == expected — the #181 lift
* gate; pre-fix this row's asserttyped bailed before cgen.
* c. cstage .s == wwstage .s byte-identical — rule-10 symmetry,
* proves the checker change doesn't perturb the codegen.
*
* GATE POLARITY: must stay GREEN. A red here means either the
* checker N_CALL fn-VALUE route regressed, the cgen deref-call
* lowering regressed (#180/#185), or stage symmetry drifted.
*/
#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 expected_exit;
int stage_mask;
};
static const struct row rows[] = {
{ "minimal",
"fn add1(x: i32) i32 = { return x + 1; };\n"
"export fn main() i32 = {\n"
" let f: *fn(x: i32) i32 = &add1;\n"
" return (*f)(7);\n"
"};\n",
8,
STAGE_CS | STAGE_WW },
/* Branched-callee: the if-arm forces a runtime choice between
* two distinct fn addresses, defeating any constant-aliasing
* mask of the deref miscompile (#105 lesson). */
{ "branched_callee",
"fn aa(x: i32) i32 = { return x; };\n"
"fn bb(x: i32) i32 = { return x + 100; };\n"
"export fn main() i32 = {\n"
" let pick: i32 = 1;\n"
" let f: *fn(x: i32) i32 = &aa;\n"
" if (pick != 0) { f = &bb; };\n"
" return (*f)(7);\n"
"};\n",
107,
STAGE_CS | STAGE_WW },
{ "alias_chain",
"fn add1(x: i32) i32 = { return x + 1; };\n"
"export fn main() i32 = {\n"
" let f: *fn(x: i32) i32 = &add1;\n"
" let g: *fn(x: i32) i32 = f;\n"
" return (*g)(7);\n"
"};\n",
8,
STAGE_CS | STAGE_WW },
{ "fn_with_args",
"fn many(a: i32, b: i32, p: *i32) i32 = { return a + b + *p; };\n"
"export fn main() i32 = {\n"
" let z: i32 = 5;\n"
" let f: *fn(a: i32, b: i32, p: *i32) i32 = &many;\n"
" return (*f)(3, 7, &z);\n"
"};\n",
15,
STAGE_CS | STAGE_WW },
/* Tuple-return: exercises the multi-register return ABI through
* the deref-call. (i32, i32) keeps it simple while still covering
* the multi-reg path. */
{ "fn_tuple_return",
"fn pair(x: i32) (i32, i32) = { return (x, x + 1); };\n"
"export fn main() i32 = {\n"
" let f: *fn(x: i32) (i32, i32) = &pair;\n"
" let a, b = (*f)(7);\n"
" return a + b;\n"
"};\n",
15,
STAGE_CS | STAGE_WW },
};
static int
write_source(const char *src_path, const char *src)
{
FILE *f = fopen(src_path, "wb");
if (!f) return -1;
fputs(src, f);
fclose(f);
return 0;
}
static void
cleanup_tmp(const char *tmpdir, const char *base)
{
char p[512];
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);
snprintf(p, sizeof p, "%s/%s", 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
build_via_driver(const char *driver, const char *tmpdir, const char *src)
{
char cmd[1024];
/* #93 sep layout: emit asm to <src-stem>.sepwork/__root.s; pin
* WW_PKGCACHE under tmpdir so out/.pkgcache is untouched. */
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 + run; returns the binary's exit code (-1 on
* build failure). */
static int
run_row(const char *driver, const struct row *r, int seq)
{
char tmpdir[256], src[256], base[64], outbin[512];
snprintf(tmpdir, sizeof tmpdir, "/tmp/wcsfdc_%d_d_%d", getpid(), seq);
snprintf(src, sizeof src, "%s/main766.ww", tmpdir);
snprintf(base, sizeof base, "main766");
mkdir(tmpdir, 0755);
if (write_source(src, r->src) != 0) {
cleanup_tmp(tmpdir, base);
return -1;
}
int rc = -1;
if (build_via_driver(driver, tmpdir, src) == 0) {
snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base);
rc = runwait(outbin);
}
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 (filed bug per 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[256], tdc[256], tdw[256], base[64], cs[512], ws[512];
snprintf(tdc, sizeof tdc, "/tmp/wcsfdc_%d_c_%d", getpid(), seq);
snprintf(tdw, sizeof tdw, "/tmp/wcsfdc_%d_w_%d", getpid(), seq);
snprintf(base, sizeof base, "main766");
mkdir(tdc, 0755);
mkdir(tdw, 0755);
snprintf(src, sizeof src, "%s/main766.ww", tdc);
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/main766.ww", tdw);
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 absbin[1024];
if (bin[0] != '/') {
char cwd[1024];
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
bin = absbin;
}
char cdrv[1024], wdrv[1024];
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, &rows[i], seq++);
if (got != rows[i].expected_exit) {
fprintf(stderr,
"star_fn_deref_call[cstage run][%s]: exit=%d want=%d\n",
rows[i].label, got, rows[i].expected_exit);
fail++;
}
}
if (wwpresent && (rows[i].stage_mask & STAGE_WW)) {
total++;
int got = run_row(wdrv, &rows[i], seq++);
if (got != rows[i].expected_exit) {
fprintf(stderr,
"star_fn_deref_call[wwstage run][%s]: exit=%d want=%d\n",
rows[i].label, got, rows[i].expected_exit);
fail++;
}
total++;
if (asm_byte_identical(cdrv, wdrv, &rows[i], seq++) != 0) {
fprintf(stderr,
"star_fn_deref_call[byte-id][%s]: cstage vs wwstage asm differs\n",
rows[i].label);
fail++;
}
}
}
if (!wwpresent)
fprintf(stderr, "star_fn_deref_call: skip wwstage (no %s)\n", wdrv);
if (fail) {
fprintf(stderr, "star_fn_deref_call: %d/%d fixtures failed\n",
fail, total);
return 1;
}
printf("star_fn_deref_call: %d/%d ok\n", total, total);
return 0;
}