diff --git a/Makefile b/Makefile index cf79638c..44f35988 100644 --- a/Makefile +++ b/Makefile @@ -314,6 +314,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_inferred_struct_arg_push \ $(BIN)/test_struct_abi_size \ $(BIN)/test_typeeq_fn_ast \ + $(BIN)/test_amp_fn_ident \ $(BIN)/test_use_promote_alias \ $(BIN)/test_field_signed $(BIN)/test_frame_argcount \ $(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \ @@ -605,6 +606,12 @@ $(BIN)/test_typeeq_fn_ast: test/wcc/763_typeeq_fn_ast.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_amp_fn_ident: test/wcc/764_amp_fn_ident.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_arrlit_str_full: test/wcc/711_arrlit_str_full.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index fd9c1504..1802b774 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -2435,8 +2435,23 @@ cgexpr(Cg *c, Node *n, Local *locals) Node *opnd = n->lhs; if (opnd && opnd->kind == N_IDENT) { int off = localfind(locals, opnd->str); + Type *ot = opnd->type; + Type *ou = (ot && ot->kind == TY_NAMED) + ? ot->under : ot; if (off != 0) { ins2(c, A_LEAQ, amem(D_BP, off), areg(D_AX)); + } else if (ou && ou->kind == TY_FN) { + /* #180: address-of a top-level fn name. + * Twin of the N_IDENT TY_FN read-arm at + * line 2330 (mafn with c->cur_mod hint). + * Previously this fell through silently — + * the AX-store at the assign site picked + * up whatever AX held from prior code, so + * `let f = &add1; (*f)(7)` jumped through + * stale AX. */ + ins2(c, A_LEAQ, + mafn(c, opnd->str, c->cur_mod), + areg(D_AX)); } else if (let_islet(opnd->str) || def_isstructdef(opnd->str) || def_isarraydef(opnd->str) diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index e5e159a8..318f424d 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -20249,6 +20249,17 @@ fn cgun(c: *cgen, n: *node) void = { emitline("(BP), AX\n"); return; }; + // #180: address-of a top-level fn name. Twin of + // the N_IDENT value-of-fn read-arm in cgident + // (LEAQ + emitfnname(c, nm, c.curmod)). Previously + // fell through silently — the AX-store at the + // assign site picked up whatever AX held. + if (fnretlookup(c, nm) != nil) { + emitline("\tLEAQ\t"); + emitfnname(c, nm, c.curmod); + emitline("(SB), AX\n"); + return; + }; if (isletvar(c, nm)) { emitline("\tLEAQ\t"); emitsymname(c, nm); diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 417dd37c..0a3f9e19 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -2698,6 +2698,17 @@ fn cgun(c: *cgen, n: *node) void = { emitline("(BP), AX\n"); return; }; + // #180: address-of a top-level fn name. Twin of + // the N_IDENT value-of-fn read-arm in cgident + // (LEAQ + emitfnname(c, nm, c.curmod)). Previously + // fell through silently — the AX-store at the + // assign site picked up whatever AX held. + if (fnretlookup(c, nm) != nil) { + emitline("\tLEAQ\t"); + emitfnname(c, nm, c.curmod); + emitline("(SB), AX\n"); + return; + }; if (isletvar(c, nm)) { emitline("\tLEAQ\t"); emitsymname(c, nm); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 10fe3ac6..0339a421 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -20249,6 +20249,17 @@ fn cgun(c: *cgen, n: *node) void = { emitline("(BP), AX\n"); return; }; + // #180: address-of a top-level fn name. Twin of + // the N_IDENT value-of-fn read-arm in cgident + // (LEAQ + emitfnname(c, nm, c.curmod)). Previously + // fell through silently — the AX-store at the + // assign site picked up whatever AX held. + if (fnretlookup(c, nm) != nil) { + emitline("\tLEAQ\t"); + emitfnname(c, nm, c.curmod); + emitline("(SB), AX\n"); + return; + }; if (isletvar(c, nm)) { emitline("\tLEAQ\t"); emitsymname(c, nm); diff --git a/test/wcc/764_amp_fn_ident.c b/test/wcc/764_amp_fn_ident.c new file mode 100644 index 00000000..183409ab --- /dev/null +++ b/test/wcc/764_amp_fn_ident.c @@ -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 (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 +#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; +} + +/* 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 /.ww; for cross-mod + * (row 6) the secondary module lives at // + * .ww. modname/modsrc NULL for single-file rows. */ + const char *src; + const char *modname; + const char *modsrc; + /* expected_leaq: the LEAQ (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 ` build ` in ; 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 has at least one line containing + * . */ +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; +}