cgen: deref of *fn skips MOVQ load — pointer IS fn-addr (#185)
Pre-fix the 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 read the first instruction word, and the subsequent CALL AX jumped through that junk address and segfaulted. Cstage: 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. Wwstage twin in selfhost/cmd/wcc/cgenexpr.ww cgun TK_STAR walks the n.type_ tinfo chain the same way (TY_NAMED peel then TY_FN check) and returns before the generic load. Mirrors ref/harec/src/check.c expr_call's STORAGE_POINTER→STORAGE_FUNCTION path (harec skips the deref since the pointer IS the address). Both stages must land together per rule-10 (cstage-only would break 990-997 byte-id gates — same lesson as #180). Probe: test/wcc/765_star_fn_deref.c, 5 rows table-driven — minimal / branched-callee / alias-chain / fn-with-args / fn-tuple-return. Every row is cstage-only via stage_mask because wwstage's checker bails asserttyped on `(*f)(...)` (filed as #181 — N_CALL type_ stamp gap on deref-call); #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 because both stages drop the SAME instruction symmetrically, so cs.s == ww.s holds either way. Runtime exit-code is the only correctness net here. Combined.ww regenerated for selfhost/cmd/{w6c,wwdump}/main. combined.ww per #110 freshness gate.
This commit is contained in:
6
Makefile
6
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 \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
239
test/wcc/765_star_fn_deref.c
Normal file
239
test/wcc/765_star_fn_deref.c
Normal file
@@ -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 <stdio.h>
|
||||
#include <stdlib.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 — 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;
|
||||
}
|
||||
Reference in New Issue
Block a user