1
0

c-fnptr-synthesizer.test.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  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).
  14. */
  15. import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  16. import * as fs from 'node:fs';
  17. import * as path from 'node:path';
  18. import * as os from 'node:os';
  19. import { CodeGraph } from '../src';
  20. describe('c-fnptr dispatch synthesizer', () => {
  21. let dir: string;
  22. beforeEach(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cfp-')); });
  23. afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); });
  24. const write = (rel: string, body: string) => {
  25. const p = path.join(dir, rel);
  26. fs.mkdirSync(path.dirname(p), { recursive: true });
  27. fs.writeFileSync(p, body);
  28. };
  29. const load = async () => {
  30. const cg = await CodeGraph.init(dir, { silent: true });
  31. await cg.indexAll();
  32. const db = (cg as any).db.db;
  33. const edges: { src: string; tgt: string; via: string }[] = db
  34. .prepare(
  35. `SELECT s.name src, t.name tgt, json_extract(e.metadata,'$.via') via
  36. FROM edges e JOIN nodes s ON s.id = e.source JOIN nodes t ON t.id = e.target
  37. WHERE json_extract(e.metadata,'$.synthesizedBy') = 'fn-pointer-dispatch'`
  38. )
  39. .all();
  40. cg.close?.();
  41. return edges;
  42. };
  43. const has = (edges: any[], src: string, tgt: string) => edges.some((e) => e.src === src && e.tgt === tgt);
  44. it('bridges a {name, fn} command table dispatched through p->fn() (the git shape)', async () => {
  45. write('cmd.c', `
  46. struct cmd { const char *name; int (*fn)(int argc); };
  47. static int cmd_add(int argc) { return argc + 1; }
  48. static int cmd_rm(int argc) { return argc - 1; }
  49. static int cmd_noop(int argc) { return argc; } /* defined, NOT in the table */
  50. static struct cmd commands[] = {
  51. { "add", cmd_add },
  52. { "rm", cmd_rm },
  53. };
  54. int run_builtin(struct cmd *p, int argc) {
  55. return p->fn(argc);
  56. }
  57. `);
  58. const edges = await load();
  59. expect(has(edges, 'run_builtin', 'cmd_add')).toBe(true);
  60. expect(has(edges, 'run_builtin', 'cmd_rm')).toBe(true);
  61. expect(edges.every((e) => e.via === 'cmd.fn')).toBe(true);
  62. // PRECISION: a function not registered in the table is never a target.
  63. expect(has(edges, 'run_builtin', 'cmd_noop')).toBe(false);
  64. });
  65. it('bridges designated-init (.handler = fn) and by-value c.fn() dispatch', async () => {
  66. write('ops.c', `
  67. struct ops { int (*handler)(void); int size; };
  68. static int on_open(void) { return 1; }
  69. static struct ops the_ops = { .handler = on_open, .size = 4 };
  70. int dispatch(struct ops o) { return o.handler(); }
  71. `);
  72. const edges = await load();
  73. expect(has(edges, 'dispatch', 'on_open')).toBe(true);
  74. expect(edges.every((e) => e.via === 'ops.handler')).toBe(true);
  75. });
  76. it('bridges the typedef-field + field←field double-hop (the hook_demo.c shape)', async () => {
  77. write('hook.c', `
  78. typedef void (*hook_func)(void);
  79. struct hooks { hook_func func; };
  80. struct entry { const char *name; hook_func fn; };
  81. static void hk_set(void) {}
  82. static void hk_get(void) {}
  83. static const struct entry registry[] = {
  84. { "set", hk_set },
  85. { "get", hk_get },
  86. };
  87. void call(struct hooks *h, const struct entry *found) {
  88. h->func = found->fn; /* generic slot reassigned from the registry */
  89. h->func(); /* dispatch through hooks.func */
  90. }
  91. `);
  92. const edges = await load();
  93. // hooks.func has no direct registration; it inherits entry.fn's via h->func = found->fn.
  94. expect(has(edges, 'call', 'hk_set')).toBe(true);
  95. expect(has(edges, 'call', 'hk_get')).toBe(true);
  96. });
  97. it('keys by (struct, field): distinct fn-pointer fields do not cross-bleed', async () => {
  98. write('vtable.c', `
  99. struct io { int (*read)(void); int (*write)(int); };
  100. static int do_read(void) { return 0; }
  101. static int do_write(int x) { return x; }
  102. static struct io io = { .read = do_read, .write = do_write };
  103. int only_reads(struct io *p) { return p->read(); }
  104. `);
  105. const edges = await load();
  106. // only_reads dispatches ->read → do_read, and must NOT reach do_write (a different field).
  107. expect(has(edges, 'only_reads', 'do_read')).toBe(true);
  108. expect(has(edges, 'only_reads', 'do_write')).toBe(false);
  109. });
  110. it('does not bridge a plain data field, and no-ops on a struct with no dispatch', async () => {
  111. write('data.c', `
  112. struct box { int count; int (*fn)(void); };
  113. static int helper(void) { return 0; }
  114. static struct box b = { .count = 3, .fn = helper };
  115. /* reads a data field and never dispatches the fn pointer */
  116. int total(struct box *x) { return x->count + 1; }
  117. `);
  118. const edges = await load();
  119. // No indirect dispatch happens, so there are no synthesized edges at all.
  120. expect(edges.length).toBe(0);
  121. });
  122. it('is a no-op on a project with no C/C++ (clean control)', async () => {
  123. write('app.js', `
  124. const handlers = { add: (x) => x + 1, rm: (x) => x - 1 };
  125. function run(name, x) { return handlers[name](x); }
  126. `);
  127. const edges = await load();
  128. expect(edges.length).toBe(0);
  129. });
  130. });