Files
ww/test/wcc/765_star_fn_deref.c
Hojun-Cho a95a7a316b test/wcc: carrier ownership repair and driver-contract adaptation
Every surviving carrier now owns its artifacts: checked mkdir/mkdtemp/
fopen acquisition, one all-exit cleanup funnel per carrier, ENOENT-
tolerant checked unlinks, exact-path deletion (rm -rf only for an
owned pid-keyed dir or a .sepwork beneath one), and cleanup failure
fails a passing carrier without overwriting its diagnostic. In the
same pass the carriers adapt to the driver contract this branch lands:
--sep and WW_PKGCACHE are gone, -S and the /tmp/ww_run_<pid> scratch
contract are asserted, and rows whose runtime or reject coverage moved
to test/wcc/data fixtures or test/lang @test owners are trimmed to the
byte/artifact/diagnostic observations only they can make.

Repair and adaptation ride together because most files interleave both
in the same hunks; splitting would manufacture intermediate carrier
states that never existed and cannot run against either driver.
2026-08-07 23:21:04 +09:00

248 lines
7.8 KiB
C

/*
* 765_star_fn_deref — root-cause lock for project #185. Pre-fix master
* (5478695 baseline, post-#180), the cgen N_UN TK_STAR arm applied
* the generic pointer-load `MOVQ (AX), AX` to a *fn operand. cgexpr
* on the operand already left AX = fn-addr (post-#180 LEAQ); the
* spurious second load then read the first instruction word and the
* subsequent CALL AX jumped through that junk address, segfaulting.
*
* Fix: cmd/w6c/cgen.c N_UN TK_STAR opens with a TY_NAMED-peel +
* TY_FN early-break — leave AX as the fn-addr cgexpr produced.
* Selfhost twin in selfhost/cmd/wcc/cgenexpr.ww cgun TK_STAR uses the
* same shape via tinfo.kind walk (TY_NAMED peel then TY_FN check),
* mirroring ref/harec/src/check.c expr_call's
* STORAGE_POINTER→STORAGE_FUNCTION path (harec skips the deref since
* the pointer IS the address).
*
* #181 deferral: wwstage's checker bails asserttyped on the
* `(*f)(...)` deref-call N_CALL shape (filed as project #181 — type_
* stamp gap on the call-of-derefed-fn-ptr). Until #181 lands the
* wwstage rows can't even reach cgen, so every row in this fixture
* is gated CSTAGE-ONLY via stage_mask. Symmetry of the wwstage cgen
* change is covered indirectly by the 990-997 self-build byte-id
* gates (any cgen-side asymmetry would surface there); #181's own
* probe will lock the wwstage runtime once the bail lifts.
*
* GATE-BLIND RISK (ken's note): byte-id alone cannot catch this
* class — both stages drop the SAME instruction symmetrically, so
* cs.s == ww.s holds either way. Runtime exit-code is the only
* correctness net here, hence the multi-row table covering callee
* shapes that exercise distinct ABI codepaths.
*
* Coverage:
* 1. minimal — `let f = &add1; (*f)(7) == 8`
* 2. branched_callee — pick aa or bb by runtime cond; verify
* the SELECTED fn is actually invoked
* (#105 lesson: single-callee masks via
* address-constant aliasing)
* 3. alias_chain — `let f = &fn; let g = f; (*g)(7)` —
* catches if cgen re-evaluates *f after
* the assignment
* 4. fn_with_args — multiple args, scalar + ptr mix; ABI
* register layout survives the deref-
* call
* 5. fn_tuple_return — (i64, str) return shape; multi-reg
* return ABI survives the deref-call
*
* Gates per row:
* a. cstage builds (exit 0); proves the checker stamps the
* deref-call N_CALL on the cstage side.
* b. cstage runs and exits with the row's expected_exit; pre-fix
* every row was 139 (SIGSEGV).
*
* GATE POLARITY: must stay GREEN. A red here means either the cgen
* TK_STAR TY_FN early-break regressed, or stage symmetry drifted
* far enough to surface here (unlikely while wwstage rows stay
* gated off).
*/
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include "wwtestpkg.h"
static int
runwait(const char *cmd)
{
int rc = system(cmd);
if (rc == -1) return -1;
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
return -1;
}
/* stage_mask bits — every row is cstage-only until #181 lifts the
* wwstage asserttyped bail on `(*f)(x)`. STAGE_WW defined for
* structural parity with 764. */
#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 },
/* 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 },
{ "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 },
{ "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 },
/* Tuple-return: exercises the multi-register return ABI through
* the deref-call. Tagged size 16B+ would also hit the AX:DX:CX
* convention; (i64, str) 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 },
};
static int
write_source(const char *src_path, const char *src)
{
FILE *f = fopen(src_path, "wb");
if (!f) return -1;
wwtest_fputs(src, f);
int rc = ferror(f) ? -1 : 0;
if (fclose(f) != 0) rc = -1;
return rc;
}
static int
cleanup_tmp(const char *tmpdir)
{
char p[512];
int rc = 0;
snprintf(p, sizeof p, "%s/main765.ww", tmpdir);
if (unlink(p) != 0 && errno != ENOENT) rc = -1;
snprintf(p, sizeof p, "%s/main765", tmpdir);
if (unlink(p) != 0 && errno != ENOENT) rc = -1;
snprintf(p, sizeof p, "rm -rf %s/main765.sepwork", tmpdir);
if (runwait(p) != 0) rc = -1;
if (rmdir(tmpdir) != 0) rc = -1;
return rc;
}
static int
build_via_driver(const char *driver, const char *outbin, const char *src)
{
char cmd[1024];
snprintf(cmd, sizeof cmd, "timeout 180 %s build -o %s %s 2>/dev/null",
driver, outbin, src);
return runwait(cmd);
}
/* run_row — build + run; returns the binary's exit code (-1 on
* build failure). expected_exit comparison done at the call site. */
static int
run_row(const char *driver, const struct row *r, int seq)
{
(void)seq;
char tmpdir[] = "/tmp/wcsfd_XXXXXX";
char src[256], outbin[512];
if (mkdtemp(tmpdir) == NULL) return -1;
snprintf(src, sizeof src, "%s/main765.ww", tmpdir);
snprintf(outbin, sizeof outbin, "%s/main765", tmpdir);
int rc = -1;
if (write_source(src, r->src) != 0) {
goto out;
}
if (build_via_driver(driver, outbin, src) == 0)
rc = runwait(outbin);
out:
if (cleanup_tmp(tmpdir) != 0) {
fprintf(stderr, "star_fn_deref[%s]: cleanup failed\n", r->label);
rc = -1;
}
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];
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
int n = (int)(sizeof rows / sizeof rows[0]);
int total = 0, fail = 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[cstage][%s]: exit=%d want=%d\n",
rows[i].label, got, rows[i].expected_exit);
fail++;
}
}
/* STAGE_WW path intentionally absent — see header #181
* deferral note. When #181 lands and the wwstage runtime
* gate flips on, mirror the 764 wdrv runwait pattern. */
}
if (fail) {
fprintf(stderr, "star_fn_deref: %d/%d fixtures failed\n",
fail, total);
return 1;
}
printf("star_fn_deref: %d/%d ok\n", total, total);
return 0;
}