c-fnptr-synthesizer.test.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. /**
  2. * C/C++ function-pointer dispatch synthesis (#932).
  3. *
  4. * C polymorphism is the function pointer: a struct fn-pointer field, registered
  5. * to concrete functions in a table (positional `{"add", cmd_add}` or designated
  6. * `.fn = cmd_add`) or by assignment, then dispatched indirectly (`p->fn(argv)`).
  7. * Static extraction sees neither the registration→field binding nor the
  8. * indirect call, so the dispatcher→handler edge is missing. These tests prove
  9. * the bridge keyed by (struct type, fn-pointer field): the command-table shape,
  10. * designated init, the typedef'd-field + field←field double-hop (the issue's
  11. * own hook_demo.c shape), by-value dispatch, and the precision boundaries
  12. * (a data field is never bridged, distinct fn-pointer fields don't cross-bleed,
  13. * and a non-C project is a no-op). Plus the BARE ARRAY of function pointers
  14. * (no struct, no field) keyed by the array variable name — the opcode-table
  15. * shape `opcodes[op](…)`, the designated + cast-wrapped form with a
  16. * calling-convention typedef, same-named file-local arrays resolving without a
  17. * cross-file leak, and a registered-but-never-dispatched array (the control).
  18. */
  19. import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  20. import * as fs from 'node:fs';
  21. import * as path from 'node:path';
  22. import * as os from 'node:os';
  23. import { CodeGraph } from '../src';
  24. describe('c-fnptr dispatch synthesizer', () => {
  25. let dir: string;
  26. beforeEach(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cfp-')); });
  27. afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); });
  28. const write = (rel: string, body: string) => {
  29. const p = path.join(dir, rel);
  30. fs.mkdirSync(path.dirname(p), { recursive: true });
  31. fs.writeFileSync(p, body);
  32. };
  33. const load = async () => {
  34. const cg = await CodeGraph.init(dir, { silent: true });
  35. await cg.indexAll();
  36. const db = (cg as any).db.db;
  37. const edges: { src: string; tgt: string; via: string }[] = db
  38. .prepare(
  39. `SELECT s.name src, t.name tgt, json_extract(e.metadata,'$.via') via
  40. FROM edges e JOIN nodes s ON s.id = e.source JOIN nodes t ON t.id = e.target
  41. WHERE json_extract(e.metadata,'$.synthesizedBy') = 'fn-pointer-dispatch'`
  42. )
  43. .all();
  44. cg.close?.();
  45. return edges;
  46. };
  47. const has = (edges: any[], src: string, tgt: string) => edges.some((e) => e.src === src && e.tgt === tgt);
  48. it('bridges a {name, fn} command table dispatched through p->fn() (the git shape)', async () => {
  49. write('cmd.c', `
  50. struct cmd { const char *name; int (*fn)(int argc); };
  51. static int cmd_add(int argc) { return argc + 1; }
  52. static int cmd_rm(int argc) { return argc - 1; }
  53. static int cmd_noop(int argc) { return argc; } /* defined, NOT in the table */
  54. static struct cmd commands[] = {
  55. { "add", cmd_add },
  56. { "rm", cmd_rm },
  57. };
  58. int run_builtin(struct cmd *p, int argc) {
  59. return p->fn(argc);
  60. }
  61. `);
  62. const edges = await load();
  63. expect(has(edges, 'run_builtin', 'cmd_add')).toBe(true);
  64. expect(has(edges, 'run_builtin', 'cmd_rm')).toBe(true);
  65. expect(edges.every((e) => e.via === 'cmd.fn')).toBe(true);
  66. // PRECISION: a function not registered in the table is never a target.
  67. expect(has(edges, 'run_builtin', 'cmd_noop')).toBe(false);
  68. });
  69. it('bridges designated-init (.handler = fn) and by-value c.fn() dispatch', async () => {
  70. write('ops.c', `
  71. struct ops { int (*handler)(void); int size; };
  72. static int on_open(void) { return 1; }
  73. static struct ops the_ops = { .handler = on_open, .size = 4 };
  74. int dispatch(struct ops o) { return o.handler(); }
  75. `);
  76. const edges = await load();
  77. expect(has(edges, 'dispatch', 'on_open')).toBe(true);
  78. expect(edges.every((e) => e.via === 'ops.handler')).toBe(true);
  79. });
  80. it('bridges function-pointer fields declared in a union', async () => {
  81. write('union-ops.c', `
  82. union ops { int (*handler)(void); };
  83. static int on_open(void) { return 1; }
  84. static union ops the_ops = { .handler = on_open };
  85. int dispatch(union ops o) { return o.handler(); }
  86. `);
  87. const edges = await load();
  88. expect(has(edges, 'dispatch', 'on_open')).toBe(true);
  89. expect(edges.every((e) => e.via === 'ops.handler')).toBe(true);
  90. });
  91. it('bridges an inline union table whose entries are macro-built', async () => {
  92. write('inline-union.c', `
  93. #define SLOT(fn) { fn }
  94. static int on_open(void) { return 1; }
  95. static union inline_ops { int (*handler)(void); } ops[] = { SLOT(on_open) };
  96. int dispatch(union inline_ops o) { return o.handler(); }
  97. `);
  98. const edges = await load();
  99. expect(has(edges, 'dispatch', 'on_open')).toBe(true);
  100. });
  101. it('bridges a union table declared through an object-macro type alias', async () => {
  102. write('alias-union.c', `
  103. #define OPS_TYPE union ops
  104. #define SLOT(fn) { fn }
  105. union ops { int (*handler)(void); };
  106. static int on_open(void) { return 1; }
  107. static OPS_TYPE ops[] = { SLOT(on_open) };
  108. int dispatch(union ops o) { return o.handler(); }
  109. `);
  110. const edges = await load();
  111. expect(has(edges, 'dispatch', 'on_open')).toBe(true);
  112. });
  113. it('bridges the typedef-field + field←field double-hop (the hook_demo.c shape)', async () => {
  114. write('hook.c', `
  115. typedef void (*hook_func)(void);
  116. struct hooks { hook_func func; };
  117. struct entry { const char *name; hook_func fn; };
  118. static void hk_set(void) {}
  119. static void hk_get(void) {}
  120. static const struct entry registry[] = {
  121. { "set", hk_set },
  122. { "get", hk_get },
  123. };
  124. void call(struct hooks *h, const struct entry *found) {
  125. h->func = found->fn; /* generic slot reassigned from the registry */
  126. h->func(); /* dispatch through hooks.func */
  127. }
  128. `);
  129. const edges = await load();
  130. // hooks.func has no direct registration; it inherits entry.fn's via h->func = found->fn.
  131. expect(has(edges, 'call', 'hk_set')).toBe(true);
  132. expect(has(edges, 'call', 'hk_get')).toBe(true);
  133. });
  134. it('keys by (struct, field): distinct fn-pointer fields do not cross-bleed', async () => {
  135. write('vtable.c', `
  136. struct io { int (*read)(void); int (*write)(int); };
  137. static int do_read(void) { return 0; }
  138. static int do_write(int x) { return x; }
  139. static struct io io = { .read = do_read, .write = do_write };
  140. int only_reads(struct io *p) { return p->read(); }
  141. `);
  142. const edges = await load();
  143. // only_reads dispatches ->read → do_read, and must NOT reach do_write (a different field).
  144. expect(has(edges, 'only_reads', 'do_read')).toBe(true);
  145. expect(has(edges, 'only_reads', 'do_write')).toBe(false);
  146. });
  147. it('does not bridge a plain data field, and no-ops on a struct with no dispatch', async () => {
  148. write('data.c', `
  149. struct box { int count; int (*fn)(void); };
  150. static int helper(void) { return 0; }
  151. static struct box b = { .count = 3, .fn = helper };
  152. /* reads a data field and never dispatches the fn pointer */
  153. int total(struct box *x) { return x->count + 1; }
  154. `);
  155. const edges = await load();
  156. // No indirect dispatch happens, so there are no synthesized edges at all.
  157. expect(edges.length).toBe(0);
  158. });
  159. it('is a no-op on a project with no C/C++ (clean control)', async () => {
  160. write('app.js', `
  161. const handlers = { add: (x) => x + 1, rm: (x) => x - 1 };
  162. function run(name, x) { return handlers[name](x); }
  163. `);
  164. const edges = await load();
  165. expect(edges.length).toBe(0);
  166. });
  167. // The redis command-table shape, minimized: the handler is wrapped in a
  168. // function-like macro, the table's struct type is an object-like macro alias,
  169. // the fn-pointer field uses a function-TYPE typedef, and the dispatch receiver
  170. // is a chained field access through a multi-declarator field.
  171. it('bridges a macro-built table with a typedef field, type-alias macro, and chained dispatch', async () => {
  172. write('reg.h', `
  173. typedef void cmdProc(int x); /* function-TYPE typedef, not (*name) */
  174. struct command { const char *name; cmdProc *proc; };
  175. struct context { int id; struct command *cmd, *last; }; /* multi-declarator field */
  176. `);
  177. write('reg.c', `
  178. #include "reg.h"
  179. #define ENTRY(nm, handler) nm, handler /* function-like macro wrapping the handler */
  180. #define CMD_T command /* object-like macro: the struct-type alias */
  181. static void getCmd(int x) {}
  182. static void setCmd(int x) {}
  183. static void unusedCmd(int x) {} /* defined, NOT in the table */
  184. static struct CMD_T table[] = {
  185. { ENTRY("get", getCmd) },
  186. { ENTRY("set", setCmd) },
  187. };
  188. void run(struct context *ctx, int x) { ctx->cmd->proc(x); } /* context.cmd → command → proc */
  189. `);
  190. const edges = await load();
  191. expect(has(edges, 'run', 'getCmd')).toBe(true);
  192. expect(has(edges, 'run', 'setCmd')).toBe(true);
  193. expect(edges.every((e) => e.via === 'command.proc')).toBe(true);
  194. // PRECISION: a function not registered in the table is never a target.
  195. expect(has(edges, 'run', 'unusedCmd')).toBe(false);
  196. });
  197. // redis generates its command table into a `.def` that is #included (and never
  198. // indexed on its own). The synthesizer reads the included file with the
  199. // includer's macros in scope so the table still resolves.
  200. it('reads a macro-built table from a non-indexed #included file', async () => {
  201. write('inc.h', `
  202. typedef int opRun(void);
  203. struct op { const char *name; opRun *run; };
  204. `);
  205. write('inc.c', `
  206. #include "inc.h"
  207. #define MK(nm, fn) nm, fn
  208. #define CMD_T op
  209. static int a_impl(void){return 0;}
  210. static int b_impl(void){return 0;}
  211. #include "ops.def"
  212. int go(struct op *o) { return o->run(); }
  213. `);
  214. // `.def` is not a C source extension, so this file is never indexed — it is
  215. // only visible to the synthesizer through inc.c's #include.
  216. write('ops.def', `
  217. static struct CMD_T optable[] = {
  218. { MK("a", a_impl) },
  219. { MK("b", b_impl) },
  220. };
  221. `);
  222. const edges = await load();
  223. expect(has(edges, 'go', 'a_impl')).toBe(true);
  224. expect(has(edges, 'go', 'b_impl')).toBe(true);
  225. expect(edges.every((e) => e.via === 'op.run')).toBe(true);
  226. });
  227. // The sqlite builtin-function-table shape: the table-building macro lives in a
  228. // header (`sqliteInt.h`), separate from the file with the table (`func.c`), and
  229. // expands to a whole brace-wrapped struct element `{ …, xFunc, … }`.
  230. it('expands a header-defined macro that produces a brace-wrapped element', async () => {
  231. write('fn.h', `
  232. typedef void sqlFn(int *ctx);
  233. struct FuncDef { int nArg; sqlFn *xFunc; const char *zName; };
  234. #define MKFUNC(name, impl) { 1, impl, #name }
  235. `);
  236. write('fn.c', `
  237. #include "fn.h"
  238. static void absImpl(int *ctx) {}
  239. static void lenImpl(int *ctx) {}
  240. static struct FuncDef builtins[] = {
  241. MKFUNC(abs, absImpl),
  242. MKFUNC(len, lenImpl),
  243. };
  244. void invoke(struct FuncDef *p, int *x) { p->xFunc(x); }
  245. `);
  246. const edges = await load();
  247. expect(has(edges, 'invoke', 'absImpl')).toBe(true);
  248. expect(has(edges, 'invoke', 'lenImpl')).toBe(true);
  249. expect(edges.every((e) => e.via === 'FuncDef.xFunc')).toBe(true);
  250. });
  251. // The vim command-table shape: a table-building macro and the struct are both
  252. // behind `#ifdef`, defined INLINE with the array (`struct cmd_entry {…} table[]`)
  253. // in a header that a `.c` #includes after setting the switch macro, and the
  254. // dispatch is a parenthesized array subscript through the file-scope table
  255. // (`(cmd_table[i].handler)(x)`). Exercises #ifdef evaluation, the conditionally
  256. // redefined macro, the inline struct (never a node), and array/global dispatch.
  257. it('bridges an #ifdef-guarded inline-struct table dispatched by array subscript', async () => {
  258. write('cmds.h', `
  259. #ifdef DECLARE_TABLE
  260. # define CMD(id, name, fn) { name, fn }
  261. typedef void (*cmd_fn)(int arg);
  262. static struct cmd_entry { const char *cmd_name; cmd_fn handler; } cmd_table[] =
  263. #else
  264. # define CMD(id, name, fn) id
  265. enum cmd_id
  266. #endif
  267. {
  268. CMD(C_a, "a", do_a),
  269. CMD(C_b, "b", do_b),
  270. };
  271. `);
  272. write('main.c', `
  273. #define DECLARE_TABLE
  274. #include "cmds.h"
  275. static void do_a(int arg) {}
  276. static void do_b(int arg) {}
  277. static void unused(int arg) {} /* defined, NOT in the table */
  278. void run(int idx, int x) { (cmd_table[idx].handler)(x); }
  279. `);
  280. const edges = await load();
  281. expect(has(edges, 'run', 'do_a')).toBe(true);
  282. expect(has(edges, 'run', 'do_b')).toBe(true);
  283. expect(edges.every((e) => e.via === 'cmd_entry.handler')).toBe(true);
  284. expect(has(edges, 'run', 'unused')).toBe(false);
  285. });
  286. // A bare ARRAY of function pointers — no struct, no field. The element type is
  287. // a function-TYPE typedef (`op_t *opcodes[]`), entries are literal function
  288. // names, and dispatch is a plain subscript-then-call `opcodes[op](…)` (the
  289. // SameBoy CPU opcode-table shape). Keyed by the array variable name.
  290. it('bridges a bare array of function pointers dispatched by subscript (the opcode-table shape)', async () => {
  291. write('cpu.c', `
  292. typedef void op_t(int *vm, unsigned char opcode);
  293. static void nop(int *vm, unsigned char opcode) {}
  294. static void inc(int *vm, unsigned char opcode) {}
  295. static void unreg(int *vm, unsigned char opcode) {} /* defined, NOT in the table */
  296. static op_t *opcodes[256] = { nop, inc };
  297. void cpu_run(int *vm) {
  298. unsigned char opcode = 0;
  299. opcodes[opcode](vm, opcode);
  300. }
  301. `);
  302. const edges = await load();
  303. expect(has(edges, 'cpu_run', 'nop')).toBe(true);
  304. expect(has(edges, 'cpu_run', 'inc')).toBe(true);
  305. expect(edges.every((e) => e.via === 'opcodes[]')).toBe(true);
  306. // PRECISION: a function not in the array is never a target.
  307. expect(has(edges, 'cpu_run', 'unreg')).toBe(false);
  308. });
  309. // The php Zend shape: a function-POINTER typedef whose declarator carries a
  310. // calling-convention macro before the `*` (`(FASTCALL *dtor_t)`), an array of
  311. // it filled by DESIGNATED index with CAST-wrapped entries (`[1] = (dtor_t)fn`),
  312. // dispatched through a subscript whose index is itself a call (`t[type(p)](p)`).
  313. it('bridges a designated + cast-wrapped array with a calling-convention typedef (the Zend dtor shape)', async () => {
  314. write('rc.c', `
  315. #define FASTCALL
  316. typedef void (FASTCALL *dtor_t)(int *p);
  317. static void empty_dtor(int *p) {}
  318. static void str_dtor(int *p) {}
  319. static void arr_dtor(int *p) {}
  320. static int type_of(int *p) { return 0; }
  321. static const dtor_t rc_dtor[] = {
  322. [0] = (dtor_t)empty_dtor,
  323. [1] = (dtor_t)str_dtor,
  324. [2] = (dtor_t)arr_dtor,
  325. };
  326. void rc_free(int *p) { rc_dtor[type_of(p)](p); }
  327. `);
  328. const edges = await load();
  329. expect(has(edges, 'rc_free', 'empty_dtor')).toBe(true);
  330. expect(has(edges, 'rc_free', 'str_dtor')).toBe(true);
  331. expect(has(edges, 'rc_free', 'arr_dtor')).toBe(true);
  332. expect(edges.every((e) => e.via === 'rc_dtor[]')).toBe(true);
  333. });
  334. // Two file-local `static` arrays share the same name across files (SameBoy
  335. // declares `opcodes[256]` in both the CPU and the disassembler). Dispatch must
  336. // resolve to the SAME file's table — no cross-file leak.
  337. it('resolves same-named file-local arrays to their own file (no cross-file leak)', async () => {
  338. write('a.c', `
  339. typedef void af_t(int *m);
  340. static void a_one(int *m) {}
  341. static void a_two(int *m) {}
  342. static af_t *table[8] = { a_one, a_two };
  343. void a_run(int *m, int i) { table[i](m); }
  344. `);
  345. write('b.c', `
  346. typedef void bf_t(int *m);
  347. static void b_one(int *m) {}
  348. static void b_two(int *m) {}
  349. static bf_t *table[8] = { b_one, b_two };
  350. void b_run(int *m, int i) { table[i](m); }
  351. `);
  352. const edges = await load();
  353. expect(has(edges, 'a_run', 'a_one')).toBe(true);
  354. expect(has(edges, 'a_run', 'a_two')).toBe(true);
  355. expect(has(edges, 'b_run', 'b_one')).toBe(true);
  356. // PRECISION: a_run's `table` is a.c's, never b.c's (and vice versa).
  357. expect(has(edges, 'a_run', 'b_one')).toBe(false);
  358. expect(has(edges, 'b_run', 'a_one')).toBe(false);
  359. });
  360. // PRECISION: an array of function pointers that is REGISTERED elsewhere (passed
  361. // by element to a registrar) but never C-dispatched `arr[i](…)` yields nothing
  362. // — the lua `package.searchers` shape, where elements are pushed into the VM.
  363. it('does not bridge a fn-pointer array that is registered, not dispatched (the searchers control)', async () => {
  364. write('pkg.c', `
  365. typedef int searcher_t(int *L);
  366. static int s_preload(int *L) { return 0; }
  367. static int s_lua(int *L) { return 0; }
  368. static searcher_t *searchers[] = { s_preload, s_lua, 0 };
  369. extern void register_one(int *L, searcher_t *s);
  370. void setup(int *L) {
  371. for (int i = 0; searchers[i]; i++) register_one(L, searchers[i]);
  372. }
  373. `);
  374. const edges = await load();
  375. expect(edges.length).toBe(0);
  376. });
  377. // This is the pass that parks the "Linking dynamic dispatch" bar on C-heavy
  378. // repos, so it reports a within-pass fraction of its file sweeps. Pin that
  379. // the fractions arrive, stay in (0, 1], and never go backwards.
  380. it('reports a monotonic within-pass progress fraction over its file sweeps', async () => {
  381. // Enough files to cross the per-16-files reporting cadence several times
  382. // across the four file sweeps.
  383. for (let i = 0; i < 33; i++) write(`f${i}.c`, `void fn${i}(void) { }\n`);
  384. const cg = await CodeGraph.init(dir, { silent: true });
  385. await cg.indexAll();
  386. const { cFnPointerDispatchEdges } = await import('../src/resolution/c-fnptr-synthesizer');
  387. const fractions: number[] = [];
  388. await cFnPointerDispatchEdges(
  389. (cg as any).queries,
  390. (cg as any).resolver.context,
  391. async () => {},
  392. (f: number) => fractions.push(f)
  393. );
  394. cg.close?.();
  395. expect(fractions.length).toBeGreaterThanOrEqual(4);
  396. for (const f of fractions) {
  397. expect(f).toBeGreaterThan(0);
  398. expect(f).toBeLessThanOrEqual(1);
  399. }
  400. for (let i = 1; i < fractions.length; i++) {
  401. expect(fractions[i]!).toBeGreaterThanOrEqual(fractions[i - 1]!);
  402. }
  403. });
  404. });