/* * 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) * * Runtime for the five symmetric rows now lives in r764_* fixtures. * This artifact wrapper retains: * a. cstage .s contains the expected `LEAQ (SB)` line — * pre-fix that line is absent (silent drop). * b. cstage .s == wwstage .s byte-identical — symmetry gate per * CLAUDE.md rule 10. * c. the cross-module C-stage-only runtime gate pending #184. * * 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 #include #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 — 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; /* Symmetric rows use their corpus source as the sole fixture body. */ const char *fixture; /* 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", "test/wcc/data/r764_amp_fn_ident_minimal/case.ww", NULL, NULL, NULL, "LEAQ\tmain.add1(SB)", STAGE_CS | STAGE_WW }, { "branched_callee", "test/wcc/data/r764_amp_fn_ident_branched/case.ww", NULL, NULL, NULL, "LEAQ\tmain.bb(SB)", STAGE_CS | STAGE_WW }, { "alias_chain", "test/wcc/data/r764_amp_fn_ident_alias_chain/case.ww", NULL, NULL, NULL, "LEAQ\tmain.add1(SB)", STAGE_CS | STAGE_WW }, { "fn_with_args", "test/wcc/data/r764_amp_fn_ident_args/case.ww", NULL, NULL, NULL, "LEAQ\tmain.many(SB)", STAGE_CS | STAGE_WW }, { "fn_tuple_return", "test/wcc/data/r764_amp_fn_ident_tuple_return/case.ww", NULL, NULL, NULL, "LEAQ\tmain.pair(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. M1 #22: `wcamffn764mod` is a directory package, so its * exported `somefn` path-mangles to `wcamffn764mod.somefn` (the * pre-M1 bare `somefn` is gone, mirroring 989_m1mangle_sym); the * &-of hint routes through use_hint and the LEAQ matches the * mangled definition. */ { "cross_module", NULL, "import wcamffn764mod;\n" "export fn main() i32 = {\n" " let f = &wcamffn764mod.somefn;\n" " return 0;\n" "};\n", "wcamffn764mod", "package wcamffn764mod;\n" "export fn somefn(x: i32) i32 = { return x + 1; };\n", "LEAQ\twcamffn764mod.somefn(SB)", STAGE_CS }, }; static int cleanup_sources(const struct row *, const char *, const char *, int); /* write_sources — copies a migrated fixture into an isolated package-main * root, or writes the one asymmetric cross-module row into that tree. */ 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) { (void)seq; int moddir_owned = 0; snprintf(tmpdir, tdsz, "/tmp/wcamffn_XXXXXX"); if (mkdtemp(tmpdir) == NULL) return -1; snprintf(src, srcsz, "%s/main764.ww", tmpdir); snprintf(base_out, basz, "main764"); if (r->fixture != NULL) { FILE *in = fopen(r->fixture, "rb"); if (in == NULL) goto fail; if (fseek(in, 0, SEEK_END) != 0) { fclose(in); goto fail; } long n = ftell(in); if (n < 0 || fseek(in, 0, SEEK_SET) != 0) { fclose(in); goto fail; } char *body = malloc((size_t)n + 1); if (body == NULL) { fclose(in); goto fail; } size_t got = fread(body, 1, (size_t)n, in); int readerr = ferror(in); fclose(in); if (got != (size_t)n || readerr) { free(body); goto fail; } body[got] = '\0'; FILE *out = fopen(src, "wb"); if (out == NULL) { free(body); goto fail; } wwtest_fputs(body, out); int wr = ferror(out) ? -1 : 0; free(body); if (fclose(out) != 0) wr = -1; if (wr != 0) goto fail; return 0; } 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); if (mkdir(moddir, 0755) != 0) goto fail; moddir_owned = 1; FILE *mf = fopen(modfile, "wb"); if (!mf) goto fail; wwtest_fputs(r->modsrc, mf); int wr = ferror(mf) ? -1 : 0; if (fclose(mf) != 0) wr = -1; if (wr != 0) goto fail; } FILE *f = fopen(src, "wb"); if (!f) goto fail; wwtest_fputs(r->src, f); int wr = ferror(f) ? -1 : 0; if (fclose(f) != 0) wr = -1; if (wr != 0) goto fail; return 0; fail: if (cleanup_sources(r, tmpdir, "main764", moddir_owned) != 0) fprintf(stderr, "amp_fn_ident: setup cleanup failed for %s\n", tmpdir); return -1; } static int cleanup_sources(const struct row *r, const char *tmpdir, const char *base, int moddir_owned) { char p[512]; int rc = 0; snprintf(p, sizeof p, "%s/%s.ww", tmpdir, base); if (unlink(p) != 0 && errno != ENOENT) rc = -1; snprintf(p, sizeof p, "%s/%s", tmpdir, base); if (unlink(p) != 0 && errno != ENOENT) rc = -1; /* #93: the sep scratch dir + the tmpdir-owned outputs. */ snprintf(p, sizeof p, "rm -rf %s/%s.sepwork", tmpdir, base); if (runwait(p) != 0) rc = -1; if (moddir_owned) { char moddir[256]; snprintf(moddir, sizeof moddir, "%s/%s", tmpdir, r->modname); snprintf(p, sizeof p, "%s/%s.ww", moddir, r->modname); if (unlink(p) != 0 && errno != ENOENT) rc = -1; if (rmdir(moddir) != 0 && errno != ENOENT) rc = -1; } if (rmdir(tmpdir) != 0) rc = -1; return rc; } /* compile_via_driver — runs compiler-only driver output in ; returns * the compiler exit code without assembling or linking. */ static int compile_via_driver(const char *driver, const char *tmpdir, const char *src) { /* #93 sep layout: `-o ` emits the asm to * .sepwork/__root.s (the post-flip layout); all outputs stay * under tmpdir. */ char cmd[2048]; snprintf(cmd, sizeof cmd, "cd %s && timeout 180 %s build -S -o %s/main764 %s " "2>/dev/null", tmpdir, driver, tmpdir, src); return runwait(cmd); } static int build_via_driver(const char *driver, const char *tmpdir, const char *src) { char stem[512], cmd[1280]; snprintf(stem, sizeof stem, "%s", src); char *dot = strrchr(stem, '.'); if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; snprintf(cmd, sizeof cmd, "cd %s && timeout 180 %s build -o %s %s " "2>/dev/null", tmpdir, driver, stem, 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; } if (cleanup_sources(r, tmpdir, base, r->modname != NULL) != 0) { fprintf(stderr, "amp_fn_ident[%s]: cleanup failed\n", r->label); rc = -1; } 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 (compile_via_driver(driver, tmpdir, src) == 0) { snprintf(asmf, sizeof asmf, "%s/%s.sepwork/__root.s", tmpdir, base); if (file_contains(asmf, r->expected_leaq)) rc = 0; } if (cleanup_sources(r, tmpdir, base, r->modname != NULL) != 0) { fprintf(stderr, "amp_fn_ident[%s]: cleanup failed\n", r->label); rc = -1; } 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; int have_tdw = 0; if (compile_via_driver(cdrv, tdc, src) != 0) goto out; snprintf(cs, sizeof cs, "%s/%s.sepwork/__root.s", tdc, base); /* Build a parallel tree so each driver's explicitly retained * .sepwork/__root.s remains available for comparison. */ if (write_sources(r, src, sizeof src, tdw, sizeof tdw, seq + 100000, base, sizeof base) != 0) goto out; have_tdw = 1; if (compile_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: if (have_tdw && cleanup_sources(r, tdw, base, r->modname != NULL) != 0) { fprintf(stderr, "amp_fn_ident[%s]: ww cleanup failed\n", r->label); rc = -1; } if (cleanup_sources(r, tdc, base, r->modname != NULL) != 0) { fprintf(stderr, "amp_fn_ident[%s]: c 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], 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++) { /* Symmetric runtime rows are owned by r764_* fixtures. Keep * only the cross-module C-stage-only runtime evidence here. */ if ((rows[i].stage_mask & STAGE_CS) && !(rows[i].stage_mask & STAGE_WW)) { 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++; } } if (rows[i].stage_mask & STAGE_CS) { 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 (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; }