feat(c/c++): resolve function-pointer command tables — macro-built, conditional-compilation & bare arrays (#991) (#1003)
* feat(c/c++): resolve macro-built function-pointer command tables (#991) C/C++ commands dispatched through macro-built function-pointer tables were dead-ends in the graph: redis' `call` never showed up as a caller of any command (`c->cmd->proc(c)`), because the table is generated into a #included `.def`, the handler is buried inside `MAKE_CMD(...)`, the struct type is itself a macro alias, the `proc` field uses a function-TYPE typedef, and the receiver is a chained field access. #954 deferred exactly this shape. Six composable additions to c-fnptr-synthesizer.ts close it: - function-type typedefs (`typedef RET T(...)` + `T *f`) flag the field as a function pointer; - multi-declarator fields (`struct redisCommand *cmd, *last`) each count as a slot/type (needed for positional alignment and the chain walk); - chained/array receivers (`c->cmd->proc`) resolve through field types across all same-named struct layouts (redis has two unrelated `client` structs); - `#include "x"` directives are followed (from raw source) so a non-indexed `.def` is read as a registration unit with the includer's effective macro env; - function-like + object-like macros are expanded (params->args, type aliases) before positional/designated registration; - a macro that expands to a brace-wrapped element (sqlite `FUNCTION(...)`) has one outer brace layer peeled. Validated on two independent macro-table lineages at 100% target precision: redis (209 commands via redisCommand.proc, `call`->every command) and sqlite (69 FuncDef.xSFunc targets). No regression on the controls: git (cmd_struct.fn, 138 builtins), curl (Curl_cftype.*), lua (0). 0 non-function targets across all five; +3 synthetic fixtures; full suite green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(c/c++): resolve conditional-compilation command tables (vim) (#991) Vim's `:ex` and normal-mode command tables are the hardest fn-pointer-table shape: the struct is defined INLINE with the array, the whole thing is behind `#ifdef DO_DECLARE_EXCMD`/`DO_DECLARE_NVCMD` (switched on by the includer), built by a macro the file conditionally redefines (`EXCMD`/`NVCMD` = the table element under the switch, a bare enum id otherwise), and dispatched by a parenthesized array subscript through a file-scope table: `(cmdnames[i].cmd_func)(&ea)`. Four more composable additions on top of the macro-table work: - a focused `#ifdef`/`#ifndef`/`#if defined`/`#else`/`#elif`/`#endif` evaluator drops inactive arms (unevaluable `#if EXPR` keeps its body); an indexed header is re-scanned in an includer's context only when that includer #defines a switch the header guards, with the include's macros re-read from the resolved text (the plain last-wins parse picks the wrong, enum, arm); - inline `struct TAG {…} var[] = {…}` tables whose struct never became a node are parsed in place and registered; - array-subscript receivers (`tbl[i].f`) strip the subscript and resolve the base through a global-var → struct-type map; - an optional `)` before the call covers the parenthesized `(….f)(args)` form. Validated on vim: 273 `:ex` commands (`do_one_cmd`→every command) + 67 normal-mode commands, 0 non-function targets, 0 cross-table misroute (registering both tables is what stops `normal_cmd`'s `nv_cmds[i].cmd_func` from falling back to the `cmdname` owner of the shared field name). Controls unchanged at 0 non-function (redis/sqlite/git/curl gain coverage from array/global dispatch, lua still 0); +1 synthetic fixture; full suite green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(c/c++): resolve bare arrays of function pointers (#991) The C/C++ fn-pointer synthesizer keyed everything on (struct type, fn-pointer field), so a dispatch through a bare array of function pointers — no struct, no field — was unbridged: an opcode/handler table like `static op_t *opcodes[256] = {nop,…}` invoked `opcodes[op](…)` left every handler with zero callers. Closes the last #991 deferred item. Keyed by the array VARIABLE name (a new `arrayReg`, parallel to the struct `reg`). Registration detects an array whose element type is a function typedef — a function-TYPE typedef element (`opcode_t *ops[]`, the `*` making it an array of pointers) or a function-pointer typedef element (`zend_rc_dtor_func_t t[]`) — and reads its literal entries, whether positional (`fn`/`&fn`), designated by index (`[IDX]=fn`), or cast-wrapped (`(cast)fn`). Dispatch is `tbl[i](…)` / `(*tbl[i])(…)`, gated on `tbl` being a known fn-pointer array (the precision anchor); the fan-out reaches the whole set (a runtime subscript hits any entry), like a command table. The same-file table wins on a name collision, so two file-local `static opcodes[256]` (SameBoy's CPU + disassembler) never cross. The fn-pointer typedef/field regexes now also tolerate a calling-convention macro before the `*` (`(ZEND_FASTCALL *name)`), which hardens the existing struct-field path too. Validated on two independent lineages: SameBoy (GB emulator) — 147 edges via `opcodes[]`, 0 cross-file leak; php-src (Zend) — 54 edges across 7 tables in the designated+cast+CC-typedef form. Control: lua 0 — its `lua_CFunction searchers[]` is pushed into the VM, never C-dispatched, so the call-gate fires nothing. No regression on the #991 corpus: redis (835) / sqlite (683) struct edges byte-identical, git +3 / curl +20 legitimate new bare-array edges, vim 433 with all guards holding; 0 non-function targets across all. + 4 fixtures. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
dfe13b03c8
commit
703629edc3
@@ -10,7 +10,11 @@
|
||||
* designated init, the typedef'd-field + field←field double-hop (the issue's
|
||||
* own hook_demo.c shape), by-value dispatch, and the precision boundaries
|
||||
* (a data field is never bridged, distinct fn-pointer fields don't cross-bleed,
|
||||
* and a non-C project is a no-op).
|
||||
* and a non-C project is a no-op). Plus the BARE ARRAY of function pointers
|
||||
* (no struct, no field) keyed by the array variable name — the opcode-table
|
||||
* shape `opcodes[op](…)`, the designated + cast-wrapped form with a
|
||||
* calling-convention typedef, same-named file-local arrays resolving without a
|
||||
* cross-file leak, and a registered-but-never-dispatched array (the control).
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
@@ -140,6 +144,224 @@ int total(struct box *x) { return x->count + 1; }
|
||||
write('app.js', `
|
||||
const handlers = { add: (x) => x + 1, rm: (x) => x - 1 };
|
||||
function run(name, x) { return handlers[name](x); }
|
||||
`);
|
||||
const edges = await load();
|
||||
expect(edges.length).toBe(0);
|
||||
});
|
||||
|
||||
// The redis command-table shape, minimized: the handler is wrapped in a
|
||||
// function-like macro, the table's struct type is an object-like macro alias,
|
||||
// the fn-pointer field uses a function-TYPE typedef, and the dispatch receiver
|
||||
// is a chained field access through a multi-declarator field.
|
||||
it('bridges a macro-built table with a typedef field, type-alias macro, and chained dispatch', async () => {
|
||||
write('reg.h', `
|
||||
typedef void cmdProc(int x); /* function-TYPE typedef, not (*name) */
|
||||
struct command { const char *name; cmdProc *proc; };
|
||||
struct context { int id; struct command *cmd, *last; }; /* multi-declarator field */
|
||||
`);
|
||||
write('reg.c', `
|
||||
#include "reg.h"
|
||||
#define ENTRY(nm, handler) nm, handler /* function-like macro wrapping the handler */
|
||||
#define CMD_T command /* object-like macro: the struct-type alias */
|
||||
static void getCmd(int x) {}
|
||||
static void setCmd(int x) {}
|
||||
static void unusedCmd(int x) {} /* defined, NOT in the table */
|
||||
static struct CMD_T table[] = {
|
||||
{ ENTRY("get", getCmd) },
|
||||
{ ENTRY("set", setCmd) },
|
||||
};
|
||||
void run(struct context *ctx, int x) { ctx->cmd->proc(x); } /* context.cmd → command → proc */
|
||||
`);
|
||||
const edges = await load();
|
||||
expect(has(edges, 'run', 'getCmd')).toBe(true);
|
||||
expect(has(edges, 'run', 'setCmd')).toBe(true);
|
||||
expect(edges.every((e) => e.via === 'command.proc')).toBe(true);
|
||||
// PRECISION: a function not registered in the table is never a target.
|
||||
expect(has(edges, 'run', 'unusedCmd')).toBe(false);
|
||||
});
|
||||
|
||||
// redis generates its command table into a `.def` that is #included (and never
|
||||
// indexed on its own). The synthesizer reads the included file with the
|
||||
// includer's macros in scope so the table still resolves.
|
||||
it('reads a macro-built table from a non-indexed #included file', async () => {
|
||||
write('inc.h', `
|
||||
typedef int opRun(void);
|
||||
struct op { const char *name; opRun *run; };
|
||||
`);
|
||||
write('inc.c', `
|
||||
#include "inc.h"
|
||||
#define MK(nm, fn) nm, fn
|
||||
#define CMD_T op
|
||||
static int a_impl(void){return 0;}
|
||||
static int b_impl(void){return 0;}
|
||||
#include "ops.def"
|
||||
int go(struct op *o) { return o->run(); }
|
||||
`);
|
||||
// `.def` is not a C source extension, so this file is never indexed — it is
|
||||
// only visible to the synthesizer through inc.c's #include.
|
||||
write('ops.def', `
|
||||
static struct CMD_T optable[] = {
|
||||
{ MK("a", a_impl) },
|
||||
{ MK("b", b_impl) },
|
||||
};
|
||||
`);
|
||||
const edges = await load();
|
||||
expect(has(edges, 'go', 'a_impl')).toBe(true);
|
||||
expect(has(edges, 'go', 'b_impl')).toBe(true);
|
||||
expect(edges.every((e) => e.via === 'op.run')).toBe(true);
|
||||
});
|
||||
|
||||
// The sqlite builtin-function-table shape: the table-building macro lives in a
|
||||
// header (`sqliteInt.h`), separate from the file with the table (`func.c`), and
|
||||
// expands to a whole brace-wrapped struct element `{ …, xFunc, … }`.
|
||||
it('expands a header-defined macro that produces a brace-wrapped element', async () => {
|
||||
write('fn.h', `
|
||||
typedef void sqlFn(int *ctx);
|
||||
struct FuncDef { int nArg; sqlFn *xFunc; const char *zName; };
|
||||
#define MKFUNC(name, impl) { 1, impl, #name }
|
||||
`);
|
||||
write('fn.c', `
|
||||
#include "fn.h"
|
||||
static void absImpl(int *ctx) {}
|
||||
static void lenImpl(int *ctx) {}
|
||||
static struct FuncDef builtins[] = {
|
||||
MKFUNC(abs, absImpl),
|
||||
MKFUNC(len, lenImpl),
|
||||
};
|
||||
void invoke(struct FuncDef *p, int *x) { p->xFunc(x); }
|
||||
`);
|
||||
const edges = await load();
|
||||
expect(has(edges, 'invoke', 'absImpl')).toBe(true);
|
||||
expect(has(edges, 'invoke', 'lenImpl')).toBe(true);
|
||||
expect(edges.every((e) => e.via === 'FuncDef.xFunc')).toBe(true);
|
||||
});
|
||||
|
||||
// The vim command-table shape: a table-building macro and the struct are both
|
||||
// behind `#ifdef`, defined INLINE with the array (`struct cmd_entry {…} table[]`)
|
||||
// in a header that a `.c` #includes after setting the switch macro, and the
|
||||
// dispatch is a parenthesized array subscript through the file-scope table
|
||||
// (`(cmd_table[i].handler)(x)`). Exercises #ifdef evaluation, the conditionally
|
||||
// redefined macro, the inline struct (never a node), and array/global dispatch.
|
||||
it('bridges an #ifdef-guarded inline-struct table dispatched by array subscript', async () => {
|
||||
write('cmds.h', `
|
||||
#ifdef DECLARE_TABLE
|
||||
# define CMD(id, name, fn) { name, fn }
|
||||
typedef void (*cmd_fn)(int arg);
|
||||
static struct cmd_entry { const char *cmd_name; cmd_fn handler; } cmd_table[] =
|
||||
#else
|
||||
# define CMD(id, name, fn) id
|
||||
enum cmd_id
|
||||
#endif
|
||||
{
|
||||
CMD(C_a, "a", do_a),
|
||||
CMD(C_b, "b", do_b),
|
||||
};
|
||||
`);
|
||||
write('main.c', `
|
||||
#define DECLARE_TABLE
|
||||
#include "cmds.h"
|
||||
static void do_a(int arg) {}
|
||||
static void do_b(int arg) {}
|
||||
static void unused(int arg) {} /* defined, NOT in the table */
|
||||
void run(int idx, int x) { (cmd_table[idx].handler)(x); }
|
||||
`);
|
||||
const edges = await load();
|
||||
expect(has(edges, 'run', 'do_a')).toBe(true);
|
||||
expect(has(edges, 'run', 'do_b')).toBe(true);
|
||||
expect(edges.every((e) => e.via === 'cmd_entry.handler')).toBe(true);
|
||||
expect(has(edges, 'run', 'unused')).toBe(false);
|
||||
});
|
||||
|
||||
// A bare ARRAY of function pointers — no struct, no field. The element type is
|
||||
// a function-TYPE typedef (`op_t *opcodes[]`), entries are literal function
|
||||
// names, and dispatch is a plain subscript-then-call `opcodes[op](…)` (the
|
||||
// SameBoy CPU opcode-table shape). Keyed by the array variable name.
|
||||
it('bridges a bare array of function pointers dispatched by subscript (the opcode-table shape)', async () => {
|
||||
write('cpu.c', `
|
||||
typedef void op_t(int *vm, unsigned char opcode);
|
||||
static void nop(int *vm, unsigned char opcode) {}
|
||||
static void inc(int *vm, unsigned char opcode) {}
|
||||
static void unreg(int *vm, unsigned char opcode) {} /* defined, NOT in the table */
|
||||
static op_t *opcodes[256] = { nop, inc };
|
||||
void cpu_run(int *vm) {
|
||||
unsigned char opcode = 0;
|
||||
opcodes[opcode](vm, opcode);
|
||||
}
|
||||
`);
|
||||
const edges = await load();
|
||||
expect(has(edges, 'cpu_run', 'nop')).toBe(true);
|
||||
expect(has(edges, 'cpu_run', 'inc')).toBe(true);
|
||||
expect(edges.every((e) => e.via === 'opcodes[]')).toBe(true);
|
||||
// PRECISION: a function not in the array is never a target.
|
||||
expect(has(edges, 'cpu_run', 'unreg')).toBe(false);
|
||||
});
|
||||
|
||||
// The php Zend shape: a function-POINTER typedef whose declarator carries a
|
||||
// calling-convention macro before the `*` (`(FASTCALL *dtor_t)`), an array of
|
||||
// it filled by DESIGNATED index with CAST-wrapped entries (`[1] = (dtor_t)fn`),
|
||||
// dispatched through a subscript whose index is itself a call (`t[type(p)](p)`).
|
||||
it('bridges a designated + cast-wrapped array with a calling-convention typedef (the Zend dtor shape)', async () => {
|
||||
write('rc.c', `
|
||||
#define FASTCALL
|
||||
typedef void (FASTCALL *dtor_t)(int *p);
|
||||
static void empty_dtor(int *p) {}
|
||||
static void str_dtor(int *p) {}
|
||||
static void arr_dtor(int *p) {}
|
||||
static int type_of(int *p) { return 0; }
|
||||
static const dtor_t rc_dtor[] = {
|
||||
[0] = (dtor_t)empty_dtor,
|
||||
[1] = (dtor_t)str_dtor,
|
||||
[2] = (dtor_t)arr_dtor,
|
||||
};
|
||||
void rc_free(int *p) { rc_dtor[type_of(p)](p); }
|
||||
`);
|
||||
const edges = await load();
|
||||
expect(has(edges, 'rc_free', 'empty_dtor')).toBe(true);
|
||||
expect(has(edges, 'rc_free', 'str_dtor')).toBe(true);
|
||||
expect(has(edges, 'rc_free', 'arr_dtor')).toBe(true);
|
||||
expect(edges.every((e) => e.via === 'rc_dtor[]')).toBe(true);
|
||||
});
|
||||
|
||||
// Two file-local `static` arrays share the same name across files (SameBoy
|
||||
// declares `opcodes[256]` in both the CPU and the disassembler). Dispatch must
|
||||
// resolve to the SAME file's table — no cross-file leak.
|
||||
it('resolves same-named file-local arrays to their own file (no cross-file leak)', async () => {
|
||||
write('a.c', `
|
||||
typedef void af_t(int *m);
|
||||
static void a_one(int *m) {}
|
||||
static void a_two(int *m) {}
|
||||
static af_t *table[8] = { a_one, a_two };
|
||||
void a_run(int *m, int i) { table[i](m); }
|
||||
`);
|
||||
write('b.c', `
|
||||
typedef void bf_t(int *m);
|
||||
static void b_one(int *m) {}
|
||||
static void b_two(int *m) {}
|
||||
static bf_t *table[8] = { b_one, b_two };
|
||||
void b_run(int *m, int i) { table[i](m); }
|
||||
`);
|
||||
const edges = await load();
|
||||
expect(has(edges, 'a_run', 'a_one')).toBe(true);
|
||||
expect(has(edges, 'a_run', 'a_two')).toBe(true);
|
||||
expect(has(edges, 'b_run', 'b_one')).toBe(true);
|
||||
// PRECISION: a_run's `table` is a.c's, never b.c's (and vice versa).
|
||||
expect(has(edges, 'a_run', 'b_one')).toBe(false);
|
||||
expect(has(edges, 'b_run', 'a_one')).toBe(false);
|
||||
});
|
||||
|
||||
// PRECISION: an array of function pointers that is REGISTERED elsewhere (passed
|
||||
// by element to a registrar) but never C-dispatched `arr[i](…)` yields nothing
|
||||
// — the lua `package.searchers` shape, where elements are pushed into the VM.
|
||||
it('does not bridge a fn-pointer array that is registered, not dispatched (the searchers control)', async () => {
|
||||
write('pkg.c', `
|
||||
typedef int searcher_t(int *L);
|
||||
static int s_preload(int *L) { return 0; }
|
||||
static int s_lua(int *L) { return 0; }
|
||||
static searcher_t *searchers[] = { s_preload, s_lua, 0 };
|
||||
extern void register_one(int *L, searcher_t *s);
|
||||
void setup(int *L) {
|
||||
for (int i = 0; searchers[i]; i++) register_one(L, searchers[i]);
|
||||
}
|
||||
`);
|
||||
const edges = await load();
|
||||
expect(edges.length).toBe(0);
|
||||
|
||||
Reference in New Issue
Block a user