cgen: address-of fn name emits LEAQ via mafn (#180)

Pre-fix the N_UN TK_AMP arm fell through silently when the operand
was an N_IDENT naming a top-level function — the let/def cascade
had no TY_FN branch, so the store at the assign site picked up
whatever AX held from prior code (commonly a stale arg register).
A subsequent (*f)(...) jumped through that junk and segfaulted.

Cstage: cmd/w6c/cgen.c N_UN TK_AMP IDENT adds a TY_FN arm before
the let/def cascade, mirror of the read-arm at line 2330 — same
mafn(opnd->str, c->cur_mod) shape. Wwstage twin in selfhost/cmd/
wcc/cgenexpr.ww cgun TK_AMP IDENT uses the analogous predicate
fnretlookup(c, nm) != nil + emitfnname(c, nm, c.curmod), matching
the cstage emit on byte-id. Both stages must land together per
rule-10 (cstage-only breaks 990-997 byte-id gates).

Combined.ww regenerated for selfhost/cmd/{w6c,wwdump}/main.combined
.ww per #110 freshness gate.

Probe: test/wcc/764_amp_fn_ident.c, 6 rows table-driven —
minimal / branched-callee / alias-chain / fn-with-args / fn-tuple
-return / cross-module. Rows 1-5 gate both stages (run + .s LEAQ
check + cs.s == ww.s byte-id); row 6 cross-module is cstage-only
because wwstage bails asserttyped on `&mod.fn` (sibling project
#184, filed). Per drew option (b) the probe exercises the address
-of without (*f)(7) — deref-call runtime coverage stays with
project #181's probe once the wwstage asserttyped bail on
N_CALL(*f) is lifted.

Phase 1 cross-mod verdict = FINE for cstage (LEAQ emits via the
already-present N_DOT TK_AMP branch at cgen.c:2477-2493); WWSTAGE
fails asserttyped on the same shape → project #184.
This commit is contained in:
2026-05-28 18:50:10 +09:00
parent caca68eb72
commit 5478695922
6 changed files with 439 additions and 0 deletions

384
test/wcc/764_amp_fn_ident.c Normal file
View File

@@ -0,0 +1,384 @@
/*
* 764_amp_fn_ident — root-cause lock for project #180. Pre-fix master
* (caca68e baseline), the cgen N_UN TK_AMP arm fell through silently
* when the operand was an N_IDENT naming a top-level function: the
* arm had branches for off!=0 (local), let_islet, def_isstructdef/
* arraydef/scalardef and a def_isanydef fatal, but NO TY_FN arm. The
* store at the assign site picked up whatever AX held from prior
* code (often a stale argument register), so `let f = &add1` wrote
* junk into f. A subsequent `(*f)(...)` then jumped through that
* junk and segfaulted. Sister-bug at cgen.c:2330 already had the
* TY_FN arm for the value-read of a bare ident (`let f = add1;` —
* though that ww-side spelling is rejected by the checker today);
* #180 adds the address-of twin.
*
* Fix: cmd/w6c/cgen.c N_UN TK_AMP IDENT inserts a TY_FN branch
* before the let/def cascade — `LEAQ mafn(opnd->str, c->cur_mod), AX`
* — mirror of the read-arm at line 2330. Selfhost twin in selfhost/
* cmd/wcc/cgenexpr.ww cgun TK_AMP IDENT uses `fnretlookup(c, nm) !=
* nil` as the analogous predicate (cstage tracks fn-ness via Type;
* wwstage tracks via the fnret registry — both stages resolve to
* the same LEAQ on byte-id).
*
* Phase 1 cross-mod probe (ken's mandate): `&pkg.fn` on CSTAGE
* already flows through the N_DOT TK_AMP branch (cgen.c:2477-2493)
* and emits the correct LEAQ via mafn(opnd->str, opnd->lhs->str) —
* verdict = FINE for cstage. WWSTAGE however bails asserttyped on
* the same shape (`un main.combined.ww:7`), a sibling checker gap
* filed as project #184 (wwstage N_UN TK_AMP N_DOT-mod-ident type_
* stamp missing). Row 6 below therefore runs CSTAGE-ONLY to lock
* the working cstage behaviour; wwstage cross-mod is gated on the
* stage_mask field and skipped until #184 lifts the bail.
*
* Coverage (drew option (b) — exercise the address-of without the
* deref-call; runtime coverage for `(*f)(...)` deferred to project
* #181's probe, which lifts the wwstage asserttyped bail):
* 1. minimal — `let f = &add1`
* 2. branched callee — conditionally pick one of two fns
* 3. alias chain — `let f = &fn; let g = f`
* 4. fn-with-args — multiple scalar + ptr params
* 5. fn-with-tuple-return — (i64, str) return shape
* 6. cross-module — `let f = &pkg.fn` (Phase 1 = FINE)
*
* Gates per row:
* a. cstage builds + runs (exit 0); proves the LEAQ emits and
* frame layout survives the AX-store (pre-fix this was junk
* data, not necessarily a segfault — the segfault only fired
* on the deref-call).
* b. cstage .s contains the expected `LEAQ <fnname>(SB)` line —
* pre-fix that line is absent (silent drop).
* c. wwstage builds + runs, if wwstage driver present.
* d. cstage .s == wwstage .s byte-identical — symmetry gate per
* CLAUDE.md rule 10.
*
* GATE POLARITY: must stay GREEN. A red here means either the cgen
* TK_AMP IDENT TY_FN arm regressed, or stage symmetry drifted.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.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;
}
/* stage_mask bits — controls per-row which stages this fixture
* gates. Row 6 (cross_module) is cstage-only until project #184
* lifts the wwstage asserttyped bail on `&mod.fn`. */
#define STAGE_CS 1
#define STAGE_WW 2
struct row {
const char *label;
/* Source written into <tmpdir>/<basename>.ww; for cross-mod
* (row 6) the secondary module lives at <tmpdir>/<modname>/
* <modname>.ww. modname/modsrc NULL for single-file rows. */
const char *src;
const char *modname;
const char *modsrc;
/* expected_leaq: the LEAQ <sym>(SB) symbol expected in the
* cstage emitted .s. Pre-fix this line is absent on rows 1-5.
* Row 6 already worked via the N_DOT TK_AMP path. */
const char *expected_leaq;
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 = &add1;\n"
" return 0;\n"
"};\n",
NULL, NULL,
"LEAQ\tadd1(SB)",
STAGE_CS | STAGE_WW },
{ "branched_callee",
"fn aa(x: i32) i32 = { return x; };\n"
"fn bb(x: i32) i32 = { return x + 1; };\n"
"export fn main() i32 = {\n"
" let pick: i32 = 1;\n"
" let f = &aa;\n"
" if (pick != 0) { f = &bb; };\n"
" return 0;\n"
"};\n",
NULL, NULL,
"LEAQ\tbb(SB)",
STAGE_CS | STAGE_WW },
{ "alias_chain",
"fn add1(x: i32) i32 = { return x + 1; };\n"
"export fn main() i32 = {\n"
" let f = &add1;\n"
" let g = f;\n"
" return 0;\n"
"};\n",
NULL, NULL,
"LEAQ\tadd1(SB)",
STAGE_CS | STAGE_WW },
{ "fn_with_args",
"fn many(a: i32, b: i64, p: *i32) i64 = { return b; };\n"
"export fn main() i32 = {\n"
" let f = &many;\n"
" return 0;\n"
"};\n",
NULL, NULL,
"LEAQ\tmany(SB)",
STAGE_CS | STAGE_WW },
{ "fn_tuple_return",
"fn pair() (i64, str) = { return (7: i64, \"x\"); };\n"
"export fn main() i32 = {\n"
" let f = &pair;\n"
" return 0;\n"
"};\n",
NULL, NULL,
"LEAQ\tpair(SB)",
STAGE_CS | STAGE_WW },
/* Cross-mod: cstage emits the LEAQ via N_DOT TK_AMP (already
* working pre-#180). Wwstage bails asserttyped on the same
* shape — filed as #184; this row stays cstage-only until that
* lifts. */
{ "cross_module",
"import wcamffn764mod;\n"
"export fn main() i32 = {\n"
" let f = &wcamffn764mod.somefn;\n"
" return 0;\n"
"};\n",
"wcamffn764mod",
"export fn somefn(x: i32) i32 = { return x + 1; };\n",
"LEAQ\tsomefn(SB)",
STAGE_CS },
};
/* write_sources — writes the row's main source plus, for cross-mod
* rows, the secondary module file. Returns the basename of the main
* source (without .ww) via *base_out. */
static int
write_sources(const struct row *r, char *src, size_t srcsz,
char *tmpdir, size_t tdsz, int seq, char *base_out, size_t basz)
{
snprintf(tmpdir, tdsz, "/tmp/wcamffn_%d_d_%d", getpid(), seq);
snprintf(src, srcsz, "%s/main764.ww", tmpdir);
snprintf(base_out, basz, "main764");
mkdir(tmpdir, 0755);
if (r->modname != NULL) {
char moddir[256], modfile[256];
snprintf(moddir, sizeof moddir, "%s/%s", tmpdir, r->modname);
snprintf(modfile, sizeof modfile, "%s/%s.ww",
moddir, r->modname);
mkdir(moddir, 0755);
FILE *mf = fopen(modfile, "wb");
if (!mf) return -1;
fputs(r->modsrc, mf);
fclose(mf);
}
FILE *f = fopen(src, "wb");
if (!f) return -1;
fputs(r->src, f);
fclose(f);
return 0;
}
static void
cleanup_sources(const struct row *r, 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.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);
if (r->modname != NULL) {
char moddir[256];
snprintf(moddir, sizeof moddir, "%s/%s", tmpdir, r->modname);
snprintf(p, sizeof p, "%s/%s.ww", moddir, r->modname); unlink(p);
rmdir(moddir);
}
rmdir(tmpdir);
}
/* build_via_driver — runs `<driver> build <src>` in <tmpdir>; returns
* the build exit code. */
static int
build_via_driver(const char *driver, const char *tmpdir, const char *src)
{
char cmd[1024];
snprintf(cmd, sizeof cmd, "cd %s && timeout 180 %s build %s 2>/dev/null",
tmpdir, driver, src);
return runwait(cmd);
}
/* run_row — full build+run gate via a ww driver. Returns 0 if the
* binary builds and exits 0, else -1. */
static int
run_row(const char *driver, const struct row *r, int seq)
{
char src[256], tmpdir[256], base[64], outbin[512];
if (write_sources(r, src, sizeof src, tmpdir, sizeof tmpdir,
seq, base, sizeof base) != 0)
return -1;
int rc = -1;
if (build_via_driver(driver, tmpdir, src) == 0) {
snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base);
if (runwait(outbin) == 0) rc = 0;
}
cleanup_sources(r, tmpdir, base);
return rc;
}
/* file_contains — true if file <path> has at least one line containing
* <needle>. */
static int
file_contains(const char *path, const char *needle)
{
FILE *f = fopen(path, "rb");
if (!f) return 0;
char buf[1024];
int found = 0;
while (fgets(buf, sizeof buf, f) != NULL) {
if (strstr(buf, needle) != NULL) { found = 1; break; }
}
fclose(f);
return found;
}
/* check_leaq — build the row via cstage, scan the emitted .s for the
* expected LEAQ line. Returns 0 if present, -1 otherwise. */
static int
check_leaq(const char *driver, const struct row *r, int seq)
{
char src[256], tmpdir[256], base[64], asmf[512];
if (write_sources(r, src, sizeof src, tmpdir, sizeof tmpdir,
seq, base, sizeof base) != 0)
return -1;
int rc = -1;
if (build_via_driver(driver, tmpdir, src) == 0) {
snprintf(asmf, sizeof asmf, "%s/%s.s", tmpdir, base);
if (file_contains(asmf, r->expected_leaq)) rc = 0;
}
cleanup_sources(r, tmpdir, base);
return rc;
}
/* asm_byte_identical — diff cstage vs wwstage .s for the row. */
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];
if (write_sources(r, src, sizeof src, tdc, sizeof tdc,
seq, base, sizeof base) != 0)
return -1;
int rc = -1;
if (build_via_driver(cdrv, tdc, src) != 0) goto out;
snprintf(cs, sizeof cs, "%s/%s.s", tdc, base);
/* Build a parallel tree for wwstage so we don't clobber the
* cstage .s. ww_ww writes intermediates next to the .ww source
* (filed bug per CLAUDE.md rule 14 phase split). */
if (write_sources(r, src, sizeof src, tdw, sizeof tdw,
seq + 100000, base, sizeof base) != 0)
goto out;
if (build_via_driver(wdrv, tdw, src) != 0) {
cleanup_sources(r, tdw, base);
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);
cleanup_sources(r, tdw, base);
out:
cleanup_sources(r, tdc, 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++;
if (run_row(cdrv, &rows[i], seq++) != 0) {
fprintf(stderr,
"amp_fn_ident[cstage run][%s]: build/run failed\n",
rows[i].label);
fail++;
}
total++;
if (check_leaq(cdrv, &rows[i], seq++) != 0) {
fprintf(stderr,
"amp_fn_ident[cstage leaq][%s]: missing `%s` in .s\n",
rows[i].label, rows[i].expected_leaq);
fail++;
}
}
if (wwpresent && (rows[i].stage_mask & STAGE_WW)) {
total++;
if (run_row(wdrv, &rows[i], seq++) != 0) {
fprintf(stderr,
"amp_fn_ident[wwstage run][%s]: build/run failed\n",
rows[i].label);
fail++;
}
total++;
if (asm_byte_identical(cdrv, wdrv, &rows[i], seq++) != 0) {
fprintf(stderr,
"amp_fn_ident[byte-id][%s]: cstage vs wwstage asm differs\n",
rows[i].label);
fail++;
}
}
}
if (!wwpresent)
fprintf(stderr, "amp_fn_ident: skip wwstage (no %s)\n", wdrv);
if (fail) {
fprintf(stderr, "amp_fn_ident: %d/%d fixtures failed\n",
fail, total);
return 1;
}
printf("amp_fn_ident: %d/%d ok\n", total, total);
return 0;
}