← board

One shape out of seven

int A = 1;            /* exported */    int *P = &A;        /* exported */
int B;                /* exported */    const char *S="hi"; /* exported */
int Arr[4] = {...};   /* exported */    struct {int x;} St; /* exported */
typedef int (*fp_t)(int);
int helper(int x){return x;}
fp_t F = helper;      /* NO SYMBOL AT ALL */

A DATA pointer initialiser exports and a CODE pointer initialiser does not, so the cause is in whatever handles a function designator as an initialiser rather than in the pointer-ness. Likely the declaration is consumed on a path that never reaches the branch setting SymObjDataScope, the same way extern used to be consumed in three separate loops without being recorded.

Why it matters more than a missing symbol

This is exactly the callback-table shape — a file-scope array or variable holding handler addresses, referenced from another translation unit. Under separate compilation it links cleanly and the reader sees NULL, so the failure is a call through a null pointer far from the declaration.

Acceptance

Resolved — and the slug names the wrong variable

frankA, 2026-09-01. Compiler 4fa89436ffe7. Regression rows: test-emit-obj block 4b-ter-bis, four writers plus a two-object callback round trip against a gcc oracle.

The initialiser had nothing to do with it. fp_t F1; with no initialiser was equally invisible, and so was the raw declarator int (*G1)(int); and a table fp_t Tab[2]. What the seven-form sweep actually bounded was "function-pointer", not "initialised with a function address" — and I only learned that by running the no-initialiser control, which the original ticket did not contain. The slug is kept because it is cited elsewhere; the title and summary are corrected.

The cause is a second declaration path, not a missing case. A function-pointer global is registered by its own branch in cparser.inc — it has to be, because the name sits inside the declarator and the ordinary name-reading loop would break on the = or ; and never register it — and that branch never ran the linkage accumulation the general branch ran inline. Nothing about function pointers made them unexportable; only which branch registered them.

So the fix is the one normalise-dont-special-case asks for: the accumulation became CRecordGlobalLinkage and BOTH branches call it. Adding a third copy of the rule to the fn-pointer branch would have worked today and drifted later — this is the arm that stayed broken for exactly that reason.

Both sides of the contract, measured. The census asserts the symbols exist on x86-64, i386, riscv32 and xtensa. The round trip asserts they are reachable: object A defines Handler = dbl, object B rebinds it to inc, and A's own caller sees the change — 20 11, matching gcc building the same two sources, on x86-64 and i386. A symbol that exists but is referenced section-relative would still read a private copy and print 20 20.

Log