c-fnptr-synthesizer.ts 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086
  1. /**
  2. * C/C++ function-pointer dispatch synthesis (#932).
  3. *
  4. * C/C++ polymorphism is the function pointer: a struct carries a fn-pointer
  5. * field (`int (*fn)(int)`, or a fn-pointer-typedef field `hook_func func`),
  6. * concrete functions are *registered* into it through a table
  7. * (`static struct cmd cmds[] = {{"add", cmd_add}, …}`, a designated
  8. * `.fn = cmd_add`, or `x->fn = cmd_add`), and the dispatcher calls through it
  9. * indirectly (`p->fn(argv)`). Static extraction captures neither the
  10. * registration→field binding nor the indirect call, so the dispatcher→handler
  11. * edge is missing and `git`'s `run_builtin` looks like it calls nothing, the
  12. * hooks in `hook_demo.c` are unreachable, etc.
  13. *
  14. * This bridges it, keyed by **(struct type, fn-pointer field)**:
  15. * • registrations — a function bound to `S.field` via a positional
  16. * initializer (matched by field index), a designated `.field = fn`, or a
  17. * direct `x.field = fn` / `x->field = fn` assignment;
  18. * • dispatch — `recv->field(…)` / `recv.field(…)` where `recv` resolves to a
  19. * value of struct type `S` (from the enclosing function's params / locals,
  20. * or by walking a chained/array receiver `c->cmd->proc` across field types),
  21. * falling back to the field name when it is unique to one struct;
  22. * • field←field propagation — `a->f = b->g` merges `B.g`'s handlers into
  23. * `A.f`, so a generic single-slot hook that is reassigned from a registry
  24. * (the `hook_demo.c` shape: `h->func = found->fn`) still resolves.
  25. *
  26. * Also handles **macro-built tables** (#991) — the dominant real-world shape,
  27. * e.g. redis' command table, sqlite's builtin functions, and vim's `:ex` /
  28. * normal-mode commands. The fn-pointer arg lives inside a macro call
  29. * (`MAKE_CMD(…,proc,…)` / `FUNCTION(…,xFunc)` / `EXCMD(…,fn,…)`) in a generated
  30. * or `#include`-d file; the table's struct type may itself be an object-macro
  31. * alias; the field may use a function-TYPE typedef; the struct may be defined
  32. * INLINE with the array; and the whole thing may sit behind `#ifdef` switched on
  33. * by the includer. The registration pass reads each `#include`-d file as a unit
  34. * with the includer's effective macro env (own + headers) in scope, evaluates
  35. * its `#ifdef`s against the includer's defined set, expands object/function
  36. * macros, peels a brace-wrapped element, and parses an inline struct in place —
  37. * then reads the positional/designated bindings. Dispatch additionally resolves
  38. * an array subscript through a file-scope table (`(cmdnames[i].cmd_func)(…)`).
  39. *
  40. * Also bridges **bare arrays of function pointers** (no struct, no field) —
  41. * `opcode_t *opcodes[256] = {nop,…}` dispatched `opcodes[op](…)` (SameBoy's CPU),
  42. * `zend_rc_dtor_func_t t[] = {[IS_STRING]=(cast)fn,…}` dispatched `t[GC_TYPE(p)](…)`
  43. * (php's Zend) — keyed by the array VARIABLE name. The element type must be a
  44. * function typedef (the precision gate), entries are literal function names, and
  45. * the same-file table wins on a name collision (two file-local `opcodes[256]`).
  46. *
  47. * Whole-graph pass after base resolution; all edges are `provenance:'heuristic'`
  48. * (`synthesizedBy:'fn-pointer-dispatch'`). High precision via the (type, field)
  49. * key + a real-function gate; a project with no fn-pointer dispatch is a no-op.
  50. */
  51. import * as path from 'node:path';
  52. import type { Edge, Node } from '../types';
  53. import type { QueryBuilder } from '../db/queries';
  54. import type { ResolutionContext } from './types';
  55. import type { MaybeYield } from './cooperative-yield';
  56. import { memoryBudgetBytes } from './memory-budget';
  57. import { LRUCache } from './lru-cache';
  58. import { stripCommentsForRegex } from './strip-comments';
  59. const C_CPP_EXT = /\.(c|h|cc|cpp|cxx|hpp|hh|hxx|cppm|ipp|inl|tcc)$/i;
  60. const FN_KINDS = new Set(['function', 'method']);
  61. const FANOUT_CAP = 300; // a real command table (git ~150) is legitimate fan-out; this only stops pathological cases.
  62. /** A struct field, in declaration order, flagged when it is a function pointer. */
  63. interface FieldInfo {
  64. name: string;
  65. index: number;
  66. isFnPtr: boolean;
  67. /** The field's declared type token (e.g. `redisCommand` for `struct redisCommand *cmd`),
  68. * used to walk a chained receiver `c->cmd->proc`. Empty for fn-pointer fields. */
  69. type: string;
  70. }
  71. /** Slice a node's body from a pre-split line array — the per-file sweeps (B/D/E)
  72. * call this once per NODE, and splitting the whole file per node was an
  73. * O(nodes × file-size) term (~1.6M full-file splits on the Linux tree,
  74. * §7a.3 cFnPtr round). Split once per file, slice many times. */
  75. function sliceLinesPre(lines: string[], startLine?: number, endLine?: number): string {
  76. if (!startLine) return '';
  77. return lines.slice(startLine - 1, endLine ?? startLine).join('\n');
  78. }
  79. /** Index of the `}` matching the `{` at `open` (which must point at a `{`). -1 if unbalanced. */
  80. function matchBrace(src: string, open: number): number {
  81. let depth = 0;
  82. for (let i = open; i < src.length; i++) {
  83. const c = src[i];
  84. if (c === '{') depth++;
  85. else if (c === '}') {
  86. depth--;
  87. if (depth === 0) return i;
  88. }
  89. }
  90. return -1;
  91. }
  92. /** Split `body` on `sep` at brace/paren/bracket depth 0 (commas inside `{…}` / `(…)` stay together). */
  93. function splitTopLevel(body: string, sep: string): string[] {
  94. const out: string[] = [];
  95. let depth = 0;
  96. let start = 0;
  97. for (let i = 0; i < body.length; i++) {
  98. const c = body[i]!;
  99. if (c === '{' || c === '(' || c === '[') depth++;
  100. else if (c === '}' || c === ')' || c === ']') depth--;
  101. else if (c === sep && depth === 0) {
  102. out.push(body.slice(start, i));
  103. start = i + 1;
  104. }
  105. }
  106. out.push(body.slice(start));
  107. return out;
  108. }
  109. /** Index of the `)` matching the `(` at `open` (which must point at a `(`). -1 if unbalanced. */
  110. function matchParen(src: string, open: number): number {
  111. let depth = 0;
  112. for (let i = open; i < src.length; i++) {
  113. const c = src[i];
  114. if (c === '(') depth++;
  115. else if (c === ')') {
  116. depth--;
  117. if (depth === 0) return i;
  118. }
  119. }
  120. return -1;
  121. }
  122. /** A function-like macro: `#define NAME(p0,p1,…) expansion`. */
  123. interface MacroDef {
  124. params: string[];
  125. expansion: string;
  126. }
  127. /**
  128. * Collect function-like macros from (comment-stripped) source, joining
  129. * `\`-continuations first. Only object/positional table macros matter here, so
  130. * variadic macros are skipped. Used to expand registration tables built through
  131. * a macro (redis' `MAKE_CMD(…)`) before reading the struct-field bindings.
  132. */
  133. function parseFunctionMacros(stripped: string): Map<string, MacroDef> {
  134. const out = new Map<string, MacroDef>();
  135. if (!stripped.includes('#define') && !stripped.includes('# define')) return out;
  136. const joined = stripped.replace(/\\\r?\n/g, ' ');
  137. const RE = /^[ \t]*#[ \t]*define[ \t]+(\w+)\(([^)]*)\)\s+(.+)$/gm;
  138. let m: RegExpExecArray | null;
  139. while ((m = RE.exec(joined))) {
  140. const params = m[2]!.split(',').map((p) => p.trim()).filter(Boolean);
  141. if (params.some((p) => p === '...' || p.endsWith('...'))) continue; // variadic — skip
  142. out.set(m[1]!, { params, expansion: m[3]!.trim() });
  143. }
  144. return out;
  145. }
  146. /**
  147. * Collect object-like macros `#define NAME value` (NAME not immediately followed
  148. * by `(`). redis aliases the table's struct type this way:
  149. * `#define COMMAND_STRUCT redisCommand`, used as `struct COMMAND_STRUCT table[]`.
  150. */
  151. function parseObjectMacros(stripped: string): Map<string, string> {
  152. const out = new Map<string, string>();
  153. if (!stripped.includes('#define') && !stripped.includes('# define')) return out;
  154. const joined = stripped.replace(/\\\r?\n/g, ' ');
  155. const RE = /^[ \t]*#[ \t]*define[ \t]+(\w+)[ \t]+(\S[^\n]*)$/gm;
  156. let m: RegExpExecArray | null;
  157. while ((m = RE.exec(joined))) out.set(m[1]!, m[2]!.trim());
  158. return out;
  159. }
  160. /** All macro names a file `#define`s (value-ful or not) — the "defined" set for #ifdef. */
  161. function parseDefinedNames(stripped: string): Set<string> {
  162. const out = new Set<string>();
  163. if (!stripped.includes('#define') && !stripped.includes('# define')) return out;
  164. const RE = /^[ \t]*#[ \t]*define[ \t]+(\w+)/gm;
  165. let m: RegExpExecArray | null;
  166. while ((m = RE.exec(stripped))) out.add(m[1]!);
  167. return out;
  168. }
  169. /**
  170. * Drop the inactive arms of `#ifdef`/`#ifndef`/`#if defined(X)`/`#else`/`#elif`/
  171. * `#endif` given a set of defined macro names, keeping line offsets (inactive
  172. * lines are blanked, not removed). A conditional whose expression we can't
  173. * evaluate (`#if SOME_EXPR`) keeps its body — better to over-keep than to drop
  174. * live code. This is what makes a header included with a switch macro defined
  175. * (vim's `ex_cmds.h` under `DO_DECLARE_EXCMD`) expose only its active table.
  176. */
  177. function evalConditionals(text: string, defined: Set<string>): string {
  178. if (!/#\s*if/.test(text)) return text;
  179. const lines = text.split('\n');
  180. // stack frame: parentActive = enclosing kept?; active = this arm kept?; taken = any arm taken yet
  181. const stack: { parentActive: boolean; active: boolean; taken: boolean }[] = [];
  182. const activeNow = (): boolean => (stack.length === 0 ? true : stack[stack.length - 1]!.active);
  183. const condDefined = (expr: string): boolean | null => {
  184. let mm = expr.match(/^defined\s*\(?\s*(\w+)\s*\)?$/);
  185. if (mm) return defined.has(mm[1]!);
  186. mm = expr.match(/^!\s*defined\s*\(?\s*(\w+)\s*\)?$/);
  187. if (mm) return !defined.has(mm[1]!);
  188. return null; // unevaluable
  189. };
  190. for (let i = 0; i < lines.length; i++) {
  191. const t = lines[i]!.trim();
  192. let mm: RegExpMatchArray | null;
  193. if ((mm = t.match(/^#\s*ifdef\s+(\w+)/))) {
  194. const pa = activeNow();
  195. const cond = defined.has(mm[1]!);
  196. stack.push({ parentActive: pa, active: pa && cond, taken: cond });
  197. lines[i] = '';
  198. continue;
  199. }
  200. if ((mm = t.match(/^#\s*ifndef\s+(\w+)/))) {
  201. const pa = activeNow();
  202. const cond = !defined.has(mm[1]!);
  203. stack.push({ parentActive: pa, active: pa && cond, taken: cond });
  204. lines[i] = '';
  205. continue;
  206. }
  207. if ((mm = t.match(/^#\s*if\s+(.+)$/))) {
  208. const pa = activeNow();
  209. const c = condDefined(mm[1]!.trim());
  210. const cond = c === null ? true : c; // unevaluable → keep
  211. stack.push({ parentActive: pa, active: pa && cond, taken: cond });
  212. lines[i] = '';
  213. continue;
  214. }
  215. if (/^#\s*elif\b/.test(t)) {
  216. const top = stack[stack.length - 1];
  217. if (top) { top.active = top.parentActive && !top.taken; top.taken = true; }
  218. lines[i] = '';
  219. continue;
  220. }
  221. if (/^#\s*else\b/.test(t)) {
  222. const top = stack[stack.length - 1];
  223. if (top) { top.active = top.parentActive && !top.taken; top.taken = true; }
  224. lines[i] = '';
  225. continue;
  226. }
  227. if (/^#\s*endif\b/.test(t)) {
  228. stack.pop();
  229. lines[i] = '';
  230. continue;
  231. }
  232. if (!activeNow()) lines[i] = ''; // blank an inactive line (keep the newline)
  233. }
  234. return lines.join('\n');
  235. }
  236. /** Resolve a type token through object-like macro aliases (transitive, capped). */
  237. function resolveTypeName(name: string, objEnv: Map<string, string> | undefined): string {
  238. let n = name;
  239. for (let i = 0; objEnv && i < 5; i++) {
  240. const v = objEnv.get(n);
  241. const t = v?.trim().match(/^(?:struct\s+)?(\w+)$/);
  242. if (!t) break;
  243. n = t[1]!;
  244. }
  245. return n;
  246. }
  247. /** Substitute call args for the macro's params (whole-token) in its expansion. */
  248. function substituteMacro(def: MacroDef, args: string[]): string {
  249. const map = new Map<string, string>();
  250. def.params.forEach((p, i) => map.set(p, args[i] ?? ''));
  251. return def.expansion.replace(/\b\w+\b/g, (tok) => (map.has(tok) ? map.get(tok)! : tok));
  252. }
  253. /**
  254. * Expand known function-like macro calls in `text` to a fixpoint (depth-capped).
  255. * `MAKE_CMD("get",…,getCommand,…)` → the positional value list whose slots line
  256. * up with the struct's fields, so the existing positional registration can read
  257. * `getCommand` straight out of the `proc` slot.
  258. */
  259. function expandMacroCalls(text: string, env: Map<string, MacroDef>): string {
  260. if (env.size === 0) return text;
  261. let out = text;
  262. for (let pass = 0; pass < 6; pass++) {
  263. let changed = false;
  264. const RE = /\b(\w+)\s*\(/g;
  265. let m: RegExpExecArray | null;
  266. while ((m = RE.exec(out))) {
  267. const def = env.get(m[1]!);
  268. if (!def) continue;
  269. const open = m.index + m[0].length - 1; // index of the `(`
  270. const close = matchParen(out, open);
  271. if (close < 0) continue;
  272. const args = splitTopLevel(out.slice(open + 1, close), ',').map((a) => a.trim());
  273. out = out.slice(0, m.index) + substituteMacro(def, args) + out.slice(close + 1);
  274. changed = true;
  275. break; // restart scan — offsets shifted
  276. }
  277. if (!changed) break;
  278. }
  279. return out;
  280. }
  281. /** A fn-pointer field looks like `… (*name)(…)` — capture `name`. A
  282. * calling-convention / attribute macro may precede the `*`
  283. * (`(ZEND_FASTCALL *name)`), so allow leading word tokens. */
  284. const FNPTR_DECL_RE = /\(\s*(?:\w+\s+)*\*\s*(\w+)\s*\)\s*\(/;
  285. /** `typedef RET (*NAME)(…)` — a function-pointer typedef (CC/attr macro before
  286. * the `*` allowed, as in php's `typedef void (ZEND_FASTCALL *fn_t)(…)`). */
  287. const FNPTR_TYPEDEF_RE = /\btypedef\b[^;{}]*?\(\s*(?:\w+\s+)*\*\s*(\w+)\s*\)\s*\(/g;
  288. /** A whole brace-free `typedef … ;` statement — capture the guts to spot the
  289. * function-TYPE form `typedef RET NAME(params)` (no `(*name)` pointer form). */
  290. const FNTYPE_TYPEDEF_STMT_RE = /\btypedef\b([^;{}]*);/g;
  291. /** Return-type keywords that must never be mistaken for the typedef's name. */
  292. const C_TYPE_KEYWORDS = new Set([
  293. 'void', 'int', 'char', 'short', 'long', 'unsigned', 'signed', 'float', 'double',
  294. 'const', 'struct', 'union', 'enum', 'static', 'volatile', 'register', 'inline',
  295. ]);
  296. /** `#include "local/header"` — captured from RAW source (string contents survive). */
  297. const INCLUDE_RE = /#[ \t]*include[ \t]+"([^"\n]+)"/g;
  298. /** Included files worth scanning for registration tables (e.g. a generated `.def`). */
  299. const INCLUDABLE_EXT = /\.(def|inc|h|hh|hpp|hxx|c|cc|cpp|cxx|ipp|tcc|tbl)$/i;
  300. export async function cFnPointerDispatchEdges(
  301. _queries: QueryBuilder,
  302. ctx: ResolutionContext,
  303. onYield: MaybeYield,
  304. onFraction?: (fraction: number) => void
  305. ): Promise<Edge[]> {
  306. let scannedFiles = 0;
  307. const files = ctx.getAllFiles().filter((f) => C_CPP_EXT.test(f));
  308. if (files.length === 0) return [];
  309. // CODEGRAPH_SYNTH_TIMINGS sub-attribution: this pass is 86% of kernel-scale
  310. // synthesis (306s, §7a.2/§7a.3) — per-sweep walls + read/strip accounting
  311. // name which sweep and which cost class owns it.
  312. const prof = process.env.CODEGRAPH_SYNTH_TIMINGS
  313. ? { A: 0, B: 0, C: 0, D: 0, E: 0, readMs: 0, readN: 0, stripMs: 0, stripN: 0, nodesMs: 0, nodesN: 0 }
  314. : null;
  315. // Within-pass progress: this is the pass that parks the "Linking dynamic
  316. // dispatch" bar on C-heavy repos, so it reports a real fraction of its
  317. // dominant work. `files` is swept once per file loop below (passes A, C, D,
  318. // E — pass B is node-bound and comparatively brief), reported at the same
  319. // per-16-files cadence as the cooperative yield.
  320. const FILE_SWEEPS = 4;
  321. const tick = async (): Promise<void> => {
  322. if ((++scannedFiles & 15) === 0) {
  323. onFraction?.(scannedFiles / (files.length * FILE_SWEEPS));
  324. await onYield();
  325. }
  326. };
  327. // Cache raw + stripped source per file, LRU-BOUNDED. The old unbounded Maps
  328. // retained every C/C++ file's raw AND stripped text for the whole pass —
  329. // multiple GB on the Linux kernel, one of the two OOM culprits in #1212.
  330. // Every sweep below iterates in `files` order, and node-kind scans return
  331. // rows in file-commit order, so access is near-sequential and a small LRU
  332. // hits; a miss just re-reads + re-strips.
  333. // Cache sizing is memory-budget-aware AND all-or-nothing (§7a.3 cFnPtr
  334. // round): the flat 128 caused 4.4 strips/file across the pass's four file
  335. // sweeps — 71.8s of the kernel-scale wall — and a partial LRU is WORSE than
  336. // useless for cyclic sweeps (a first attempt sized ~61k against 63.8k files
  337. // and thrashed to a ~0% cross-sweep hit rate). Hold every stripped file
  338. // (~24KB each measured on the Linux tree) only when 40% of the live memory
  339. // budget covers it; otherwise keep the old within-sweep-locality 128. The
  340. // cache is pass-scoped — freed on return.
  341. // Slack over files.length: non-indexed includes (.def/.inc, generated
  342. // headers) join the working set mid-pass, and even a handful of keys past
  343. // the cap re-triggers cyclic eviction (measured: cap==files.length still
  344. // stripped 2.25×/file). Pass-scoped transient, freed on return.
  345. const fullCacheCap = Math.ceil(files.length * 1.05) + 512;
  346. const cacheCap = memoryBudgetBytes() * 0.5 >= fullCacheCap * 24_576 ? fullCacheCap : 128;
  347. const rawCache = new LRUCache<string, string | null>(Math.min(cacheCap, 4096));
  348. const raw = (file: string): string | null => {
  349. if (rawCache.has(file)) return rawCache.get(file)!;
  350. const t0 = prof ? Date.now() : 0;
  351. const r = ctx.readFile(file);
  352. if (prof) { prof.readMs += Date.now() - t0; prof.readN++; }
  353. rawCache.set(file, r);
  354. return r;
  355. };
  356. const srcCache = new LRUCache<string, string>(cacheCap);
  357. const src = (file: string): string | null => {
  358. // A cached '' (empty or unreadable file) returns '' where the miss path
  359. // returns null for unreadable — every caller falsy-checks, so the two are
  360. // interchangeable.
  361. const hit = srcCache.get(file);
  362. if (hit !== undefined) return hit;
  363. const r = raw(file);
  364. const t0 = prof ? Date.now() : 0;
  365. const s = r == null ? '' : stripCommentsForRegex(r, 'c');
  366. if (prof) { prof.stripMs += Date.now() - t0; prof.stripN++; }
  367. srcCache.set(file, s);
  368. return r == null ? null : s;
  369. };
  370. // Resolve a quoted include relative to the includer's directory, then the
  371. // project root. Returns a project-root-relative path that exists on disk
  372. // (even if it was never indexed — e.g. redis' generated `commands.def`).
  373. const resolveInclude = (includer: string, inc: string): string | null => {
  374. const dir = path.posix.dirname(includer.replace(/\\/g, '/'));
  375. const cand = path.posix.normalize(path.posix.join(dir, inc));
  376. if (ctx.fileExists(cand)) return cand;
  377. if (ctx.fileExists(inc)) return inc;
  378. return null;
  379. };
  380. // ---- Pass A: function-pointer AND function-type typedefs (cross-file) ----
  381. // fn-pointer: typedef RET (*NAME)(…) → a field `NAME f` is a fn ptr
  382. // fn-type: typedef RET NAME(params) → a field `NAME *f` is a fn ptr
  383. // The fn-type form is redis' command idiom: `typedef void redisCommandProc(client*)`
  384. // declared as `redisCommandProc *proc;`. Without this, `proc` reads as data.
  385. const fnPtrTypedefs = new Set<string>();
  386. const fnTypeTypedefs = new Set<string>();
  387. let tPass = Date.now();
  388. for (const file of files) {
  389. await tick();
  390. const s = src(file);
  391. if (!s || !s.includes('typedef')) continue;
  392. FNPTR_TYPEDEF_RE.lastIndex = 0;
  393. let m: RegExpExecArray | null;
  394. while ((m = FNPTR_TYPEDEF_RE.exec(s))) fnPtrTypedefs.add(m[1]!);
  395. FNTYPE_TYPEDEF_STMT_RE.lastIndex = 0;
  396. while ((m = FNTYPE_TYPEDEF_STMT_RE.exec(s))) {
  397. const guts = m[1]!;
  398. if (guts.includes('(*') || guts.includes('( *')) continue; // pointer form — handled above
  399. const fm = guts.match(/\b(\w+)\s*\(/); // last identifier before the param list
  400. if (fm && !C_TYPE_KEYWORDS.has(fm[1]!)) fnTypeTypedefs.add(fm[1]!);
  401. }
  402. }
  403. if (prof) { prof.A = Date.now() - tPass; tPass = Date.now(); }
  404. // ---- Pass B: struct field layouts ----
  405. // structLayout: struct name → ordered fields, for structs with ≥1 fn-pointer
  406. // field (drives positional registration + dispatch).
  407. // allStructFields: EVERY struct name → ALL its field layouts (a name can be
  408. // reused across files — e.g. redis has two unrelated `client` structs), used
  409. // to walk a chained receiver's field types (`c->cmd->proc`: client.cmd →
  410. // redisCommand). The walk searches every same-named layout for the field.
  411. // fieldToStructs: fn-pointer field name → set of struct names that declare it.
  412. const structLayout = new Map<string, FieldInfo[]>();
  413. const allStructFields = new Map<string, FieldInfo[][]>();
  414. const fieldToStructs = new Map<string, Set<string>>();
  415. // Parse a struct body (the text between its `{` and `}`) into ordered fields.
  416. const parseStructFields = (inner: string): FieldInfo[] => {
  417. const fields: FieldInfo[] = [];
  418. let idx = 0;
  419. for (const rawDecl of splitTopLevel(inner, ';')) {
  420. const decl = rawDecl.trim();
  421. if (!decl) continue;
  422. // A field decl can declare several names sharing a leading type:
  423. // `struct redisCommand *cmd, *lastcmd;`. Each declarator is its own
  424. // positional slot and carries that type (so `client.cmd → redisCommand`).
  425. const parts = splitTopLevel(decl, ',');
  426. const firstTyped = parts[0]!.match(/(\w+)\s+\**\s*(\w+)\s*$/);
  427. const sharedType = firstTyped ? firstTyped[1]! : '';
  428. for (let pi = 0; pi < parts.length; pi++) {
  429. const p = parts[pi]!.trim();
  430. let name: string | null = null;
  431. let type = '';
  432. let isFnPtr = false;
  433. const ptr = p.match(FNPTR_DECL_RE);
  434. if (ptr) {
  435. name = ptr[1]!; // `… (*name)(…)` — a function pointer
  436. isFnPtr = true;
  437. } else if (pi === 0) {
  438. if (firstTyped) { name = firstTyped[2]!; type = sharedType; }
  439. } else {
  440. // a subsequent declarator: `*name` / `**name` / `name`
  441. const dm = p.match(/^\**\s*(\w+)/);
  442. if (dm) { name = dm[1]!; type = sharedType; }
  443. }
  444. if (!ptr && type) isFnPtr = fnPtrTypedefs.has(type) || fnTypeTypedefs.has(type);
  445. // Always advance the positional index. An unparsed field (anonymous
  446. // union, exotic declarator) still occupies one slot, and macro-expanded
  447. // positional tables (redis' MAKE_CMD) only align if every field counts.
  448. fields.push({ name: name ?? '', index: idx, isFnPtr: !!name && isFnPtr, type });
  449. idx++;
  450. }
  451. }
  452. return fields;
  453. };
  454. // Register a parsed struct under `name` into the three indexes.
  455. const registerStructLayout = (name: string, fields: FieldInfo[]): void => {
  456. if (!allStructFields.has(name)) allStructFields.set(name, []);
  457. allStructFields.get(name)!.push(fields);
  458. for (const f of fields) {
  459. if (f.name && f.isFnPtr) {
  460. if (!fieldToStructs.has(f.name)) fieldToStructs.set(f.name, new Set());
  461. fieldToStructs.get(f.name)!.add(name);
  462. }
  463. }
  464. if (fields.some((f) => f.isFnPtr)) structLayout.set(name, fields);
  465. };
  466. let linesFile = '';
  467. let linesArr: string[] = [];
  468. for (const st of (ctx.iterateNodesByKind?.('struct') ?? ctx.getNodesByKind('struct'))) {
  469. if ((++scannedFiles & 255) === 0) await onYield();
  470. if (!C_CPP_EXT.test(st.filePath)) continue;
  471. const s = src(st.filePath);
  472. if (!s) continue;
  473. if (linesFile !== st.filePath) { linesFile = st.filePath; linesArr = s.split('\n'); }
  474. const body = sliceLinesPre(linesArr, st.startLine, st.endLine);
  475. const open = body.indexOf('{');
  476. const close = open >= 0 ? matchBrace(body, open) : -1;
  477. if (open < 0 || close < 0) continue;
  478. registerStructLayout(st.name, parseStructFields(body.slice(open + 1, close)));
  479. }
  480. if (prof) { prof.B = Date.now() - tPass; tPass = Date.now(); }
  481. // NB: no early return on an empty structLayout here — an inline `struct TAG
  482. // { … } var[]` table whose struct never became a node (vim's `cmdname`, broken
  483. // up by `#ifdef`) is discovered later during the unit scan. The `reg.size === 0`
  484. // guard after registration still short-circuits when nothing bridges.
  485. const fnPtrFieldOf = (struct: string, field: string): boolean =>
  486. !!structLayout.get(struct)?.some((f) => f.name === field && f.isFnPtr);
  487. // C/C++ function + method nodes are STREAMED per sweep (see passes D/E) —
  488. // the old materialized `cFns` array held every function node on the repo
  489. // (O(nodes) memory, part of the #1212 kernel OOM).
  490. // ---- function-name → node resolution (prefer a function in the same file) ----
  491. const resolveFn = (name: string, preferFile?: string): Node | null => {
  492. const cands = ctx.getNodesByName(name).filter((n) => FN_KINDS.has(n.kind));
  493. if (cands.length === 0) return null;
  494. if (cands.length === 1) return cands[0]!;
  495. if (preferFile) {
  496. const same = cands.find((n) => n.filePath === preferFile);
  497. if (same) return same;
  498. }
  499. return cands[0]!;
  500. };
  501. // ---- Pass C: registrations — Map<"struct.field", Set<funcNodeId>> ----
  502. // Ids only — retaining the full Node per registration (the old `idToNode`)
  503. // was write-only dead weight at O(registrations) memory.
  504. const reg = new Map<string, Set<string>>();
  505. const addReg = (struct: string, field: string, fn: Node): void => {
  506. const key = `${struct}.${field}`;
  507. if (!reg.has(key)) reg.set(key, new Set());
  508. reg.get(key)!.add(fn.id);
  509. };
  510. // Bare arrays-of-fn-pointers (no struct): array VARIABLE name → per-file sets
  511. // of registered function ids. Multi-entry because a file-scope `static` table
  512. // name can recur across files (SameBoy declares `static opcode_t *opcodes[256]`
  513. // in BOTH sm83_cpu.c and sm83_disassembler.c), so dispatch resolves same-file.
  514. const arrayReg = new Map<string, { file: string; ids: Set<string> }[]>();
  515. const addArrayReg = (name: string, file: string, fn: Node): void => {
  516. let entries = arrayReg.get(name);
  517. if (!entries) { entries = []; arrayReg.set(name, entries); }
  518. let e = entries.find((x) => x.file === file);
  519. if (!e) { e = { file, ids: new Set() }; entries.push(e); }
  520. e.ids.add(fn.id);
  521. };
  522. // A struct value `{ … }` (one element) — register its function entries to the
  523. // struct's fields, by `.field = fn` designators or by positional slot.
  524. const registerStructValue = (
  525. struct: string,
  526. valueBody: string,
  527. file: string,
  528. env?: Map<string, MacroDef>,
  529. ): void => {
  530. const layout = structLayout.get(struct);
  531. if (!layout) return;
  532. if (env && env.size) valueBody = expandMacroCalls(valueBody, env);
  533. // A macro can expand to a whole brace-wrapped element (sqlite's
  534. // `FUNCTION(…)` → `{nArg, …, xFunc, …}`); peel one outer layer so the
  535. // positional slots are visible.
  536. valueBody = valueBody.trim();
  537. if (valueBody.startsWith('{')) {
  538. const e = matchBrace(valueBody, 0);
  539. if (e > 0 && valueBody.slice(e + 1).trim() === '') valueBody = valueBody.slice(1, e);
  540. }
  541. const items = splitTopLevel(valueBody, ',');
  542. let pos = 0;
  543. for (const rawItem of items) {
  544. const item = rawItem.trim();
  545. if (!item) continue;
  546. const des = item.match(/^\.\s*(\w+)\s*=\s*(?:&\s*)?(\w+)\s*$/);
  547. if (des) {
  548. const field = des[1]!;
  549. if (fnPtrFieldOf(struct, field)) {
  550. const fn = resolveFn(des[2]!, file);
  551. if (fn) addReg(struct, field, fn);
  552. }
  553. // a designated item does not advance positional counting
  554. continue;
  555. }
  556. const field = layout.find((f) => f.index === pos);
  557. if (field?.isFnPtr) {
  558. const id = item.match(/^&?\s*(\w+)\s*$/);
  559. if (id) {
  560. const fn = resolveFn(id[1]!, file);
  561. if (fn) addReg(struct, field.name, fn);
  562. }
  563. }
  564. pos++;
  565. }
  566. };
  567. // Collect the literal function entries of an array-of-fn-pointers initializer
  568. // and register them under the array's variable name. Entries may be positional
  569. // (`fn`, `&fn`), designated by index (`[OP] = fn`), or cast-wrapped
  570. // (`(handler_t)fn`, as in php's Zend dtor table). Non-identifier entries
  571. // (`NULL`, `0`, a nested expression) are skipped — a miss, never a wrong edge.
  572. // No index tracking: a runtime subscript fans the dispatch out to the whole
  573. // set, exactly like a command table reaches every command.
  574. const registerArrayValue = (
  575. name: string,
  576. body: string,
  577. file: string,
  578. env?: Map<string, MacroDef>,
  579. ): void => {
  580. if (env && env.size) body = expandMacroCalls(body, env);
  581. for (const rawItem of splitTopLevel(body, ',')) {
  582. let item = rawItem.trim();
  583. if (!item) continue;
  584. const des = item.match(/^\[[^\]]*\]\s*=\s*([\s\S]*)$/); // `[IDX] = …` designator
  585. if (des) item = des[1]!.trim();
  586. item = item.replace(/^\((?:[\w\s*]+)\)\s*/, '').replace(/^&\s*/, '').trim(); // (cast) / &
  587. const id = item.match(/^(\w+)$/);
  588. if (!id) continue;
  589. const fn = resolveFn(id[1]!, file);
  590. if (fn) addArrayReg(name, file, fn);
  591. }
  592. };
  593. // Per-file macro + include parsing (any file, indexed or not), cached.
  594. // Derived per-file caches, LRU-bounded like the content caches (#1212).
  595. const fnMacroCache = new LRUCache<string, Map<string, MacroDef>>(256);
  596. const fileFnMacros = (file: string): Map<string, MacroDef> => {
  597. let m = fnMacroCache.get(file);
  598. if (!m) { m = parseFunctionMacros(src(file) ?? ''); fnMacroCache.set(file, m); }
  599. return m;
  600. };
  601. const objMacroCache = new LRUCache<string, Map<string, string>>(256);
  602. const fileObjMacros = (file: string): Map<string, string> => {
  603. let m = objMacroCache.get(file);
  604. if (!m) { m = parseObjectMacros(src(file) ?? ''); objMacroCache.set(file, m); }
  605. return m;
  606. };
  607. const definedCache = new LRUCache<string, Set<string>>(256);
  608. const fileDefinedNames = (file: string): Set<string> => {
  609. let d = definedCache.get(file);
  610. if (!d) { d = parseDefinedNames(src(file) ?? ''); definedCache.set(file, d); }
  611. return d;
  612. };
  613. const includeCache = new LRUCache<string, string[]>(1024);
  614. const localIncludesOf = (file: string): string[] => {
  615. let out = includeCache.get(file);
  616. if (out) return out;
  617. out = [];
  618. const rawText = raw(file);
  619. if (rawText && rawText.includes('include')) {
  620. INCLUDE_RE.lastIndex = 0;
  621. let im: RegExpExecArray | null;
  622. while ((im = INCLUDE_RE.exec(rawText))) {
  623. if (!INCLUDABLE_EXT.test(im[1]!)) continue;
  624. const t = resolveInclude(file, im[1]!);
  625. if (t) out.push(t);
  626. }
  627. }
  628. includeCache.set(file, out);
  629. return out;
  630. };
  631. // A file's effective macro environment = its own #defines PLUS those of the
  632. // headers it #includes (redis' `MAKE_CMD` sits beside the table; sqlite's
  633. // `FUNCTION` lives in `sqliteInt.h`, included by the file with the table).
  634. // First writer wins, so the file's own defs override included ones; depth-2
  635. // covers a macro defined in a header-of-a-header.
  636. const buildEnv = (
  637. file: string,
  638. depth: number,
  639. seen: Set<string>,
  640. fn: Map<string, MacroDef>,
  641. obj: Map<string, string>,
  642. def: Set<string>,
  643. ): void => {
  644. if (depth < 0 || seen.has(file)) return;
  645. seen.add(file);
  646. for (const [k, v] of fileFnMacros(file)) if (!fn.has(k)) fn.set(k, v);
  647. for (const [k, v] of fileObjMacros(file)) if (!obj.has(k)) obj.set(k, v);
  648. for (const n of fileDefinedNames(file)) def.add(n);
  649. for (const inc of localIncludesOf(file)) buildEnv(inc, depth - 1, seen, fn, obj, def);
  650. };
  651. // Registration units: every indexed C file, plus the local headers/tables it
  652. // `#include`s. A non-indexed include (redis' generated `commands.def`) is
  653. // always scanned; an INDEXED header is re-scanned in an includer's context
  654. // ONLY when that includer switches on conditional code the header guards — it
  655. // `#define`s a name the header itself doesn't and the header has `#if` (vim's
  656. // `ex_cmds.h`, whose command table is behind `#ifdef DO_DECLARE_EXCMD` set by
  657. // `ex_docmd.c`). The include is scanned with the includer's effective macro
  658. // env (its `MAKE_CMD(…)` resolves there) and its conditionals evaluated
  659. // against the includer's defined set. `reg` is a Set, so unioning across
  660. // multiple includers is safe.
  661. interface Unit {
  662. text: string;
  663. file: string;
  664. env: Map<string, MacroDef>;
  665. objEnv: Map<string, string>;
  666. }
  667. const indexedSet = new Set(files);
  668. const seenInclude = new Set<string>();
  669. // Global variable → struct type, for resolving a dispatch through a file-scope
  670. // table by subscript (`cmdnames[i].cmd_func(…)`).
  671. const globalVarType = new Map<string, string>();
  672. // Process a `{ … }` initializer body (array of elements or a single struct).
  673. const processInit = (
  674. struct: string,
  675. body: string,
  676. isArray: boolean,
  677. file: string,
  678. env: Map<string, MacroDef>,
  679. ): void => {
  680. if (isArray) {
  681. for (const el of splitTopLevel(body, ',')) {
  682. const t = el.trim();
  683. if (t.startsWith('{')) {
  684. const e = matchBrace(t, 0);
  685. if (e > 0) registerStructValue(struct, t.slice(1, e), file, env);
  686. } else if (t) {
  687. // an element built by a macro (`MAKE_CMD(…)`/`FUNCTION(…)`) or a bare value
  688. registerStructValue(struct, t, file, env);
  689. }
  690. }
  691. } else {
  692. registerStructValue(struct, body, file, env);
  693. }
  694. };
  695. // `(?:struct )?TYPE name[opt] = {` initializers, where TYPE is a struct that
  696. // has ≥1 fn-pointer field. Handles both single (`= {…}`) and array
  697. // (`[] = { {…}, {…} }`) forms. Macro calls inside an element are expanded first.
  698. const INIT_RE =
  699. /(?:^|[;{}])\s*(?:(?:static|const|extern|register|volatile)\s+)*(?:struct\s+)?(\w+)\s+(\w+)\s*(\[[^\]]*\])?\s*=\s*\{/g;
  700. // `struct TAG { … } var[opt] [= {…}]` — the struct is defined INLINE with the
  701. // table (vim's `cmdname`/`nv_cmd`); its layout never became a node, so parse it
  702. // here and register it before reading the entries. No leading anchor: a
  703. // `struct TAG {` with a brace body is always a definition (it may be preceded
  704. // by a `#define …` line ending in a digit, as in vim), and the trailing
  705. // `var … = {` check below is what distinguishes a TABLE from a plain type.
  706. const INLINE_STRUCT_RE = /\bstruct\s+(\w+)\s*\{/g;
  707. // `(?:static …)* ELEMTYPE [*] name[…] = { … }` — a bare array of function
  708. // pointers (no struct wrapper). The optional `*` covers a function-TYPE
  709. // typedef element (`opcode_t *opcodes[]`); a function-pointer typedef element
  710. // (`zend_rc_dtor_func_t t[]`) needs none. The typedef-set membership gate
  711. // (below) is what separates this from a plain data/struct array.
  712. const ARRAY_TABLE_RE =
  713. /(?:^|[;{}])\s*(?:(?:static|const|extern|register|volatile)\s+)*(\w+)\s+(\*\s*)?(\w+)\s*\[[^\]]*\]\s*=\s*\{/g;
  714. // Process ONE unit's text and discard it. The old shape built every unit up
  715. // front (`const units: Unit[]`) — the full text of every C file plus its
  716. // expanded includes held simultaneously, gigabytes on the kernel (#1212).
  717. const processUnit = (unit: Unit): void => {
  718. const s = unit.text;
  719. if (!s || !s.includes('{')) return;
  720. INLINE_STRUCT_RE.lastIndex = 0;
  721. let im: RegExpExecArray | null;
  722. while ((im = INLINE_STRUCT_RE.exec(s))) {
  723. const tag = im[1]!;
  724. const sOpen = im.index + im[0].length - 1; // the struct body's `{`
  725. const sClose = matchBrace(s, sOpen);
  726. if (sClose < 0) continue;
  727. // After `}`, expect `var [opt] [= {…}]` to be a table; else it's a plain type.
  728. const after = s.slice(sClose + 1);
  729. const vm = after.match(/^\s*(\w+)\s*(\[[^\]]*\])?\s*(=\s*\{)?/);
  730. if (!vm || !vm[1]) continue;
  731. const fields = parseStructFields(s.slice(sOpen + 1, sClose));
  732. if (!fields.some((f) => f.isFnPtr)) continue; // only tables of fn pointers matter
  733. if (!structLayout.has(tag)) registerStructLayout(tag, fields);
  734. globalVarType.set(vm[1]!, tag);
  735. if (vm[3]) {
  736. const aOpen = sClose + 1 + after.indexOf('{', vm[0].length - 1);
  737. const aClose = matchBrace(s, aOpen);
  738. if (aClose > 0) {
  739. processInit(tag, s.slice(aOpen + 1, aClose), !!vm[2], unit.file, unit.env);
  740. INLINE_STRUCT_RE.lastIndex = aClose;
  741. }
  742. }
  743. }
  744. if (!s.includes('=')) return;
  745. INIT_RE.lastIndex = 0;
  746. let m: RegExpExecArray | null;
  747. while ((m = INIT_RE.exec(s))) {
  748. let struct = m[1]!;
  749. if (!structLayout.has(struct)) struct = resolveTypeName(struct, unit.objEnv);
  750. if (!structLayout.has(struct)) continue;
  751. const isArray = !!m[3];
  752. const open = m.index + m[0].length - 1; // points at the `{`
  753. const close = matchBrace(s, open);
  754. if (close < 0) continue;
  755. globalVarType.set(m[2]!, struct);
  756. processInit(struct, s.slice(open + 1, close), isArray, unit.file, unit.env);
  757. INIT_RE.lastIndex = close;
  758. }
  759. // Bare arrays-of-function-pointers (no struct, no field). Gated on the
  760. // element type being a function typedef — a fn-TYPE typedef needs the `*`
  761. // (array of pointers to it), a fn-pointer typedef does not. A data or
  762. // struct array's element type is never in these sets, so it never fires.
  763. ARRAY_TABLE_RE.lastIndex = 0;
  764. let am: RegExpExecArray | null;
  765. while ((am = ARRAY_TABLE_RE.exec(s))) {
  766. const elemType = am[1]!;
  767. const hasStar = !!am[2];
  768. if (!((fnTypeTypedefs.has(elemType) && hasStar) || fnPtrTypedefs.has(elemType))) continue;
  769. const open = am.index + am[0].length - 1; // the `{`
  770. const close = matchBrace(s, open);
  771. if (close < 0) continue;
  772. registerArrayValue(am[3]!, s.slice(open + 1, close), unit.file, unit.env);
  773. ARRAY_TABLE_RE.lastIndex = close;
  774. }
  775. };
  776. // ---- Pass C: registrations — stream each file (and its qualifying local
  777. // includes) through processUnit, one at a time.
  778. for (const file of files) {
  779. await tick();
  780. const env = new Map<string, MacroDef>();
  781. const objEnv = new Map<string, string>();
  782. const defined = new Set<string>();
  783. buildEnv(file, 2, new Set(), env, objEnv, defined);
  784. const s = src(file);
  785. if (s) processUnit({ text: s, file, env, objEnv });
  786. for (const target of localIncludesOf(file)) {
  787. if (seenInclude.has(`${file}>${target}`)) continue;
  788. const incSrc = src(target);
  789. if (!incSrc) continue;
  790. if (indexedSet.has(target)) {
  791. // Re-scan an indexed header only when this includer unlocks guarded code.
  792. const ownDef = fileDefinedNames(target);
  793. const adds = [...defined].some((n) => !ownDef.has(n));
  794. if (!adds || !/#\s*if/.test(incSrc)) continue;
  795. }
  796. seenInclude.add(`${file}>${target}`);
  797. // The include is pasted into the includer — evaluate its conditionals in
  798. // the includer's defined set (a no-op when it has none). Re-parse the
  799. // included file's OWN macros from that resolved text so a macro it defines
  800. // conditionally (vim's `EXCMD`, whose plain last-wins parse picks the enum
  801. // arm) overrides with the ARM THAT IS ACTUALLY ACTIVE here.
  802. const text = evalConditionals(incSrc, defined);
  803. const incEnv = new Map(env);
  804. for (const [k, v] of parseFunctionMacros(text)) incEnv.set(k, v);
  805. const incObjEnv = new Map(objEnv);
  806. for (const [k, v] of parseObjectMacros(text)) incObjEnv.set(k, v);
  807. processUnit({ text, file: target, env: incEnv, objEnv: incObjEnv });
  808. }
  809. }
  810. if (prof) { prof.C = Date.now() - tPass; tPass = Date.now(); }
  811. // ---- receiver-type resolution within a function's source ----
  812. // `(?:struct )?TYPE [*]recv` declared in the params or body → TYPE (if a known
  813. // fn-pointer-bearing struct).
  814. const recvReCache = new Map<string, RegExp>();
  815. const recvTypeIn = (fnSrc: string, recv: string): string | null => {
  816. let re = recvReCache.get(recv);
  817. if (!re) {
  818. re = new RegExp(`(?:struct\\s+)?(\\w+)\\s*\\*?\\s*\\b${recv}\\b\\s*(?:[,)=;]|\\[)`, 'g');
  819. recvReCache.set(recv, re);
  820. }
  821. re.lastIndex = 0;
  822. let m: RegExpExecArray | null;
  823. while ((m = re.exec(fnSrc))) {
  824. if (structLayout.has(m[1]!)) return m[1]!;
  825. }
  826. return null;
  827. };
  828. // Declared type of a local/param `v` — ANY type token, not just fn-pointer
  829. // structs (the base of a chained receiver needn't carry a fn pointer itself).
  830. // Falls back to a file-scope table variable (`cmdnames` in `cmdnames[i].fn()`).
  831. const escapeRe = (x: string): string => x.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  832. const varReCache = new Map<string, RegExp>();
  833. const varTypeIn = (fnSrc: string, v: string): string | null => {
  834. let re = varReCache.get(v);
  835. if (!re) {
  836. re = new RegExp(`(?:struct\\s+)?(\\w+)\\s*\\*?\\s*\\b${escapeRe(v)}\\b\\s*(?:[,)=;]|\\[)`, 'g');
  837. varReCache.set(v, re);
  838. }
  839. re.lastIndex = 0;
  840. let m: RegExpExecArray | null;
  841. while ((m = re.exec(fnSrc))) {
  842. if (!C_TYPE_KEYWORDS.has(m[1]!)) return m[1]!;
  843. }
  844. return globalVarType.get(v) ?? null;
  845. };
  846. // Resolve a member-access chain (`c->cmd`, or just `p`) to a struct type,
  847. // walking each segment's declared field type. `c->cmd->proc` dispatch:
  848. // base chain `c->cmd` → client.cmd's type `redisCommand`, the proc owner.
  849. // Array subscripts (`cmdnames[i]`) are stripped — an index yields one element.
  850. const resolveChainType = (fnSrc: string, chain: string): string | null => {
  851. const segs = chain.replace(/\s*\[[^\]]*\]/g, '').split(/\s*(?:->|\.)\s*/).filter(Boolean);
  852. if (segs.length === 0) return null;
  853. let t = varTypeIn(fnSrc, segs[0]!);
  854. for (let i = 1; t && i < segs.length; i++) {
  855. let next: string | null = null;
  856. for (const fields of allStructFields.get(t) ?? []) {
  857. const f = fields.find((fl) => fl.name === segs[i] && fl.type);
  858. if (f) { next = f.type; break; }
  859. }
  860. t = next;
  861. }
  862. return t;
  863. };
  864. // ---- Pass D: field←field propagation (`a->f = b->g`) ----
  865. // Collected as (targetStruct.field ← sourceStruct.field) pairs, then merged to
  866. // a fixpoint so a hook slot inherits a registry field's handlers.
  867. const FIELD_ASSIGN_RE = /(\w+)\s*(?:->|\.)\s*(\w+)\s*=\s*(\w+)\s*(?:->|\.)\s*(\w+)/g;
  868. const propagations: { to: string; from: string }[] = [];
  869. for (const file of files) {
  870. await tick();
  871. const s = src(file);
  872. if (!s || !s.includes('=')) continue;
  873. const tN = prof ? Date.now() : 0;
  874. const fnsD = ctx.getNodesInFile(file);
  875. if (prof) { prof.nodesMs += Date.now() - tN; prof.nodesN++; }
  876. const dLines = s.split('\n');
  877. for (const fn of fnsD) {
  878. if (!FN_KINDS.has(fn.kind)) continue;
  879. const body = sliceLinesPre(dLines, fn.startLine, fn.endLine);
  880. if (!body.includes('=')) continue;
  881. FIELD_ASSIGN_RE.lastIndex = 0;
  882. let m: RegExpExecArray | null;
  883. while ((m = FIELD_ASSIGN_RE.exec(body))) {
  884. const [, lrecv, lfield, rrecv, rfield] = m;
  885. // Pre-gate on field NAMES: `a->f = b->g` matches every struct-field
  886. // assignment in the tree (millions on the kernel), but only fields
  887. // that are fn-pointer fields of SOME struct can pass fnPtrFieldOf —
  888. // skip the two regex type resolutions for the ~99% that can't.
  889. if (!fieldToStructs.has(lfield!) || !fieldToStructs.has(rfield!)) continue;
  890. const lt = recvTypeIn(body, lrecv!);
  891. const rt = recvTypeIn(body, rrecv!);
  892. if (lt && rt && fnPtrFieldOf(lt, lfield!) && fnPtrFieldOf(rt, rfield!)) {
  893. propagations.push({ to: `${lt}.${lfield}`, from: `${rt}.${rfield}` });
  894. }
  895. }
  896. }
  897. }
  898. for (let pass = 0; pass < 3 && propagations.length; pass++) {
  899. let changed = false;
  900. for (const { to, from } of propagations) {
  901. const fromSet = reg.get(from);
  902. if (!fromSet) continue;
  903. if (!reg.has(to)) reg.set(to, new Set());
  904. const toSet = reg.get(to)!;
  905. for (const id of fromSet) {
  906. if (!toSet.has(id)) {
  907. toSet.add(id);
  908. changed = true;
  909. }
  910. }
  911. }
  912. if (!changed) break;
  913. }
  914. if (prof) { prof.D = Date.now() - tPass; tPass = Date.now(); }
  915. if (reg.size === 0 && arrayReg.size === 0) return [];
  916. // ---- Pass E: dispatch sites → edges ----
  917. // `base->…->field(` or `base.…field(` where `field` is a known fn-pointer field.
  918. // The base may be a chain (`c->cmd->proc`) or carry array subscripts
  919. // (`cmdnames[i].cmd_func`). An optional `)` before the call covers the
  920. // parenthesized form `(cmdnames[i].cmd_func)(&ea)` vim uses.
  921. const DISPATCH_RE = /((?:\w+(?:\s*\[[^\][]*\])?\s*(?:->|\.)\s*)+)(\w+)\s*\)?\s*\(/g;
  922. // Bare-array dispatch: `tbl[i](…)` or the explicit-deref `(*tbl[i])(…)`. The
  923. // subscript may itself contain a call (`tbl[GC_TYPE(p)](…)`), so the index
  924. // class excludes only brackets. Precision comes from the `arrayReg` gate below
  925. // — this fires only when `tbl` is a known fn-pointer array.
  926. const ARRAY_DISPATCH_RE = /(?:\(\s*\*\s*)?\b(\w+)\s*\[[^\][]*\]\s*\)?\s*\(/g;
  927. const edges: Edge[] = [];
  928. const seen = new Set<string>();
  929. for (const file of files) {
  930. await tick();
  931. const s = src(file);
  932. if (!s) continue;
  933. const tN = prof ? Date.now() : 0;
  934. const fnsE = ctx.getNodesInFile(file);
  935. if (prof) { prof.nodesMs += Date.now() - tN; prof.nodesN++; }
  936. const eLines = s.split('\n');
  937. for (const fn of fnsE) {
  938. if (!FN_KINDS.has(fn.kind)) continue;
  939. const body = sliceLinesPre(eLines, fn.startLine, fn.endLine);
  940. DISPATCH_RE.lastIndex = 0;
  941. let m: RegExpExecArray | null;
  942. let added = 0;
  943. // Incremental line counting: matches arrive in ascending index order, so
  944. // count newlines since the previous match instead of re-splitting the
  945. // whole body prefix per match (O(body) each — real time on god-files).
  946. let lcIdx = 0;
  947. let lcLine = fn.startLine;
  948. const lineAt = (idx: number): number => {
  949. for (let i = lcIdx; i < idx; i++) if (body.charCodeAt(i) === 10) lcLine++;
  950. lcIdx = idx;
  951. return lcLine;
  952. };
  953. while ((m = DISPATCH_RE.exec(body)) && added < FANOUT_CAP) {
  954. const baseChain = m[1]!.replace(/\s*(?:->|\.)\s*$/, '').trim(); // receiver, minus the trailing arrow
  955. const field = m[2]!;
  956. const owners = fieldToStructs.get(field);
  957. if (!owners || owners.size === 0) continue;
  958. // 1) resolve the receiver chain's struct type precisely (handles c->cmd->proc);
  959. // 2) else the last segment as a simple local/param of a fn-pointer-bearing struct;
  960. // 3) else fall back to a field name that belongs to exactly one struct.
  961. let struct = resolveChainType(body, baseChain);
  962. if (!struct || !owners.has(struct)) {
  963. const lastSeg = baseChain.replace(/\s*\[[^\]]*\]/g, '').split(/\s*(?:->|\.)\s*/).pop()!;
  964. const t = recvTypeIn(body, lastSeg);
  965. struct = t && owners.has(t) ? t : null;
  966. }
  967. if (!struct || !owners.has(struct)) struct = owners.size === 1 ? [...owners][0]! : null;
  968. if (!struct) continue;
  969. const targets = reg.get(`${struct}.${field}`);
  970. if (!targets) continue;
  971. const line = lineAt(m.index);
  972. for (const tid of targets) {
  973. if (tid === fn.id) continue;
  974. const key = `${fn.id}>${tid}`;
  975. if (seen.has(key)) continue;
  976. seen.add(key);
  977. edges.push({
  978. source: fn.id,
  979. target: tid,
  980. kind: 'calls',
  981. line,
  982. provenance: 'heuristic',
  983. metadata: {
  984. synthesizedBy: 'fn-pointer-dispatch',
  985. via: `${struct}.${field}`,
  986. registeredAt: `${fn.filePath}:${line}`,
  987. },
  988. });
  989. if (++added >= FANOUT_CAP) break;
  990. }
  991. }
  992. // ---- bare array-of-fn-pointers dispatch (`tbl[i](…)`) ----
  993. if (arrayReg.size && added < FANOUT_CAP) {
  994. // Fresh scan from the body's start — rewind the line-count cursor too.
  995. lcIdx = 0;
  996. lcLine = fn.startLine;
  997. ARRAY_DISPATCH_RE.lastIndex = 0;
  998. while ((m = ARRAY_DISPATCH_RE.exec(body)) && added < FANOUT_CAP) {
  999. const entries = arrayReg.get(m[1]!);
  1000. if (!entries) continue;
  1001. // Same-file table wins on a name collision (two file-local `opcodes`);
  1002. // a unique name resolves cross-file; otherwise ambiguous — bail.
  1003. const ids = entries.length === 1
  1004. ? entries[0]!.ids
  1005. : (entries.find((e) => e.file === fn.filePath)?.ids ?? null);
  1006. if (!ids) continue;
  1007. const line = lineAt(m.index);
  1008. for (const tid of ids) {
  1009. if (tid === fn.id) continue;
  1010. const key = `${fn.id}>${tid}`;
  1011. if (seen.has(key)) continue;
  1012. seen.add(key);
  1013. edges.push({
  1014. source: fn.id,
  1015. target: tid,
  1016. kind: 'calls',
  1017. line,
  1018. provenance: 'heuristic',
  1019. metadata: {
  1020. synthesizedBy: 'fn-pointer-dispatch',
  1021. via: `${m[1]}[]`,
  1022. registeredAt: `${fn.filePath}:${line}`,
  1023. },
  1024. });
  1025. if (++added >= FANOUT_CAP) break;
  1026. }
  1027. }
  1028. }
  1029. }
  1030. }
  1031. if (prof) {
  1032. prof.E = Date.now() - tPass;
  1033. console.error(
  1034. `[synth-timing] cFnPtr sub: A=${prof.A}ms B=${prof.B}ms C=${prof.C}ms D=${prof.D}ms E=${prof.E}ms | read n=${prof.readN} ${prof.readMs}ms strip n=${prof.stripN} ${prof.stripMs}ms nodesInFile n=${prof.nodesN} ${prof.nodesMs}ms`
  1035. );
  1036. }
  1037. return edges;
  1038. }