diff --git a/Makefile b/Makefile index 44f35988..7e7ad5fd 100644 --- a/Makefile +++ b/Makefile @@ -315,6 +315,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_struct_abi_size \ $(BIN)/test_typeeq_fn_ast \ $(BIN)/test_amp_fn_ident \ + $(BIN)/test_star_fn_deref \ $(BIN)/test_use_promote_alias \ $(BIN)/test_field_signed $(BIN)/test_frame_argcount \ $(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \ @@ -612,6 +613,11 @@ $(BIN)/test_amp_fn_ident: test/wcc/764_amp_fn_ident.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_star_fn_deref: test/wcc/765_star_fn_deref.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(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 1802b774..745bd0d4 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -2756,6 +2756,20 @@ cgexpr(Cg *c, Node *n, Local *locals) /* Handled in the pre-cgexpr early-exit above. */ break; case TK_STAR: /* deref */ + { + /* #185: deref of *fn — the pointer value IS + * the fn address. cgexpr(opnd) already left + * AX = fn-addr; a generic MOVQ (AX),AX would + * load the first instruction word and CALL + * would segfault on that junk. Mirror + * ref/harec/src/check.c expr_call's + * STORAGE_POINTER→STORAGE_FUNCTION skip. */ + Type *rt = n->type; + Type *ru = (rt && rt->kind == TY_NAMED) + ? rt->under : rt; + if (ru && ru->kind == TY_FN) + break; + } /* f64/f32 result rides X0 (SSE), not AX — an integer * MOVQ strands the value off the float ABI and the * caller's MOVSD X0 reads stale bits (#96). Mirrors the diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 318f424d..b6a3a091 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -20617,6 +20617,14 @@ fn cgun(c: *cgen, n: *node) void = { return; }; if (n.op == tkind.TK_STAR) { + // #185: deref of *fn — the pointer value IS the fn address. + // cgexpr(n.lhs) left AX = fn-addr; a generic MOVQ (AX),AX + // would load the first instruction word and a subsequent + // CALL would segfault. Mirror ref/harec/src/check.c + // expr_call's STORAGE_POINTER→STORAGE_FUNCTION skip. + let rti: *tinfo = n.type_: *tinfo; + for (rti != nil && rti.kind == tykind.TY_NAMED) { rti = rti.under; }; + if (rti != nil && rti.kind == tykind.TY_FN) { return; }; // f64/f32 result rides X0 (SSE), not AX — an integer MOVQ // strands the value off the float ABI and the caller's // MOVSD X0 reads stale bits (#96). Mirrors the float diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 0a3f9e19..956dae18 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -3066,6 +3066,14 @@ fn cgun(c: *cgen, n: *node) void = { return; }; if (n.op == tkind.TK_STAR) { + // #185: deref of *fn — the pointer value IS the fn address. + // cgexpr(n.lhs) left AX = fn-addr; a generic MOVQ (AX),AX + // would load the first instruction word and a subsequent + // CALL would segfault. Mirror ref/harec/src/check.c + // expr_call's STORAGE_POINTER→STORAGE_FUNCTION skip. + let rti: *tinfo = n.type_: *tinfo; + for (rti != nil && rti.kind == tykind.TY_NAMED) { rti = rti.under; }; + if (rti != nil && rti.kind == tykind.TY_FN) { return; }; // f64/f32 result rides X0 (SSE), not AX — an integer MOVQ // strands the value off the float ABI and the caller's // MOVSD X0 reads stale bits (#96). Mirrors the float diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 0339a421..a549d86e 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -20617,6 +20617,14 @@ fn cgun(c: *cgen, n: *node) void = { return; }; if (n.op == tkind.TK_STAR) { + // #185: deref of *fn — the pointer value IS the fn address. + // cgexpr(n.lhs) left AX = fn-addr; a generic MOVQ (AX),AX + // would load the first instruction word and a subsequent + // CALL would segfault. Mirror ref/harec/src/check.c + // expr_call's STORAGE_POINTER→STORAGE_FUNCTION skip. + let rti: *tinfo = n.type_: *tinfo; + for (rti != nil && rti.kind == tykind.TY_NAMED) { rti = rti.under; }; + if (rti != nil && rti.kind == tykind.TY_FN) { return; }; // f64/f32 result rides X0 (SSE), not AX — an integer MOVQ // strands the value off the float ABI and the caller's // MOVSD X0 reads stale bits (#96). Mirrors the float diff --git a/test/wcc/765_star_fn_deref.c b/test/wcc/765_star_fn_deref.c new file mode 100644 index 00000000..d49cd15c --- /dev/null +++ b/test/wcc/765_star_fn_deref.c @@ -0,0 +1,239 @@ +/* + * 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; +}