/* * 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 #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 — 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; 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.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); rmdir(tmpdir); } 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 — 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) { char tmpdir[256], src[256], base[64], outbin[512]; snprintf(tmpdir, sizeof tmpdir, "/tmp/wcsfd_%d_d_%d", getpid(), seq); snprintf(src, sizeof src, "%s/main765.ww", tmpdir); snprintf(base, sizeof base, "main765"); 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; } 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; }