Bläddra i källkod

feat(extraction): C deferral round 2 — 8 new preParse passes, linux kernel/+mm/ deferral 58.6%→33.9% (#1353)

Census-driven cut of the top-ranked post-R7a lever. All passes TS-side,
C-only (preParseCSource), shared by both arms:

- parameterized-annotation whole-blank (__free/__printf/__counted_by/
  __bpf_md_ptr…; extends through a stranded field `;`)
- type-keyword-arg scanner (kzalloc_obj(struct T), list_entry, multi-line
  continuations behind nested-paren args; bounded hand scanner, head
  exclusions + call-vs-declaration guard; blanks trailing stars)
- static/extern CAPS-macro declaration lines at any scope; the initialized
  form is REWRITTEN to its expansion (name/tail keep exact offsets)
- va_arg qualified-type blank; GNU named-variadic #define dots-only blank
  (post-restore); sandwiched notrace-family; C23 auto; multi-line
  iterator-macro spans (hlist_for_each_entry_rcu + lockdep arg)
- word list += cacheline family (2- and 4-underscore spellings) + 10 more
  census-confirmed annotations

Gates: five-repo parity sweeps 0 diffs (git deferral 16.1→12.2%, redis
25.3→24.1%, fmt/protobuf unchanged); linux full-tree both arms
2,049,153 nodes / 6,413,518 edges (+858/+6,585 vs R7a) with byte-identical
dumps (10,446,478 lines, sha256 6dd1185b); kernel-arm parse-loop 356→306s
at 2c; suite 2517 green under CODEGRAPH_KERNEL_EXPECT=1. Honesty note
recorded in the docs: error recovery was already salvaging most SYMBOLS on
deferred files — the graph win is relationships + phantom cleanup, and the
unreleased CHANGELOG entry was rewritten off the sweep-subset framing.

Also records §7a.5: post-R7a 8-core cg1212 re-run 16.4min (was 18.3min);
8c parse sits on the single-writer floor, so the <10min-on-8c gap re-ranks
to the per-ref resolution path.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Colby Mchenry 1 månad sedan
förälder
incheckning
b9d0f57a64

+ 1 - 1
CHANGELOG.md

@@ -34,7 +34,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 - Deleting a whole directory is now picked up by watch mode: the files inside it are removed from the index on the next auto-sync instead of lingering as stale records until an unrelated edit happened to trigger one. Operating systems often report a directory deletion as a single event on the directory itself (with no per-file events for its contents), which the watcher previously discarded. (#1285)
 - `codegraph sync` now gets the same slow-disk fix that made full indexing fast in 1.4.0: database checkpointing is deferred for the whole incremental run instead of firing every few megabytes of writes. On mechanical drives and other high-latency storage, a small sync on a large index no longer stalls for minutes at near-zero CPU — the cost of a sync scales with what changed, not with the size of the existing index. The same `CODEGRAPH_NO_WAL_DEFER=1` switch turns it off. (#1248)
 - C functions declared with a project-specific attribute macro in front of a typedef'd return type (`SEC_ATTR UINT32 MyFunc(VOID)` — common in embedded and kernel code) are now indexed under their real names. Previously the parser tripped over the unknown macro and stored the parameter list as the function name, leaving entries like `"(VOID)"` in the graph and making the real function unfindable. (#1211)
-- Macro-heavy C and C++ code indexes much more completely. Six ubiquitous idioms that previously tripped the parser into error recovery — dropping or garbling the surrounding symbols — now parse cleanly: the `#ifdef __cplusplus` / `extern "C" {` compatibility guard in C headers, iterator macros in statement position (`list_for_each_entry(pos, head, member) { … }` and the whole Linux-kernel/git/jemalloc family, braced or single-statement), the Linux/sparse declaration annotations (`static int __init foo(void)`, `void __user *buf`, `container_of(p, struct T, m)`), trailing parameter annotations (`int argc UNUSED`, git's house style), namespace-management macros alone on a line (`FMT_BEGIN_NAMESPACE`, Qt's `Q_OBJECT`), and function attribute macros in front of C++ return types. On git's own repository this nearly doubles the number of indexed symbols, and on the Linux kernel's `kernel/` and `mm/` directories it triples them; blast radius and callers get correspondingly more complete. A related fix stops the existing macro handling from corrupting `#define` lines that mention the same macro names, which removed a class of phantom parse errors in fmt-style headers.
+- Macro-heavy C and C++ code indexes much more completely. Fifteen ubiquitous idioms that previously tripped the parser into error recovery — dropping symbols, garbling names, minting phantom entries, or losing the relationships between real ones — now parse cleanly: the `#ifdef __cplusplus` / `extern "C" {` compatibility guard in C headers, iterator macros in statement position (`list_for_each_entry(pos, head, member) { … }` and the whole Linux-kernel/git/jemalloc family — including calls that wrap across lines, like `hlist_for_each_entry_rcu(…)` with a `lockdep_is_held` argument), the Linux/sparse declaration annotations (`static int __init foo(void)`, `void __user *buf`, `container_of(p, struct T, m)`), parameterized annotations (`__free(kfree)`, `__printf(4, 0)`, `__counted_by(count)`), per-CPU and work-queue declaration macros (`static DEFINE_PER_CPU(struct T, name);`, initialized forms included), bare type arguments to allocator and list macros (`kzalloc_obj(struct T)`, `list_entry(p, struct T, member)`), `va_arg` with a qualified or pointer type, GNU named-variadic macro definitions (`#define dbg(fmt, args...)`), `static notrace`-style compiler markers, C23 `auto` declarations, trailing parameter annotations (`int argc UNUSED`, git's house style), namespace-management macros alone on a line (`FMT_BEGIN_NAMESPACE`, Qt's `Q_OBJECT`), and function attribute macros in front of C++ return types. On the Linux kernel this cuts files lost to parse errors on the core directories by nearly half and recovers thousands of call and reference relationships that error recovery silently dropped, with git and redis seeing smaller cuts of the same kind; graphs stay byte-for-byte identical between the native and fallback engines. A related fix stops the macro handling itself from corrupting `#define` lines that mention the same macro names, which removed a class of phantom parse errors in fmt-style headers.
 - C++ methods defined out-of-line inside a namespace (`namespace sim { Output MyClass::Apply(...) { ... } }`) now carry the namespace in their qualified name, matching their class. Fully-qualified call sites from other files (`sim::MyClass::Apply(...)`) resolve to the definition again, so `codegraph callers` and file impact no longer come up empty for this pattern. (#1291)
 - C++ methods defined out-of-line on a template class (`template <typename T> T Box<T>::get() { ... }`) no longer keep the template parameter list in their qualified name. They now index as `Box::get` — identical to an inline definition of the same method — so they link to their class and resolve from call sites again, and pathological multi-line template parameter lists can no longer blow the qualified name past filesystem name limits. (#1286)
 - Go route detection no longer misidentifies ordinary method calls that share HTTP verb names — `cache.Put("key", value)`, `store.Get("config", out)`, `bus.Handle("user.created", handler)` and the like were being indexed as HTTP routes, polluting route listings in cache-heavy codebases. A registration now has to look like one: its first argument must be a `/`-prefixed path (all routers) or a Go 1.22 `"METHOD /path"` pattern on `Handle`/`HandleFunc`, which now also extracts the method instead of listing the route as `ANY`. (#1259)

+ 165 - 0
__tests__/extraction.test.ts

@@ -11280,6 +11280,171 @@ describe('C/C++ kernel-port preParse blanks (R7a)', () => {
     expect(out).toContain('__u32 count');
   });
 
+  it('blankCParameterizedAnnotationMacros blanks name+args whole, eats a stranded field semicolon', async () => {
+    const { blankCParameterizedAnnotationMacros } = await import('../src/extraction/languages/c-cpp');
+    const src = [
+      'struct file *f __free(fput) = NULL;',
+      'static void __printf(4, 0) log_it(int a, const char *fmt, ...);',
+      'struct ctx {',
+      '\t__bpf_md_ptr(struct bpf_iter_meta *, meta);',
+      '};',
+      'int keep = __hash(key);',
+      '',
+    ].join('\n');
+    const out = blankCParameterizedAnnotationMacros(src);
+    expect(out.length).toBe(src.length);
+    expect(out).not.toContain('__free');
+    expect(out).not.toContain('__printf');
+    // Mid-line match keeps its statement tail…
+    expect(out).toContain('= NULL;');
+    // …but a whole-line FIELD match eats the `;` too — a lone `;` field is
+    // itself a parse error while an empty struct body is not.
+    expect(out).not.toContain('__bpf_md_ptr');
+    expect(out.split('\n')[3]?.trim()).toBe('');
+    // Non-curated dunder calls are real code.
+    expect(out).toContain('__hash(key)');
+  });
+
+  it('blankCTypeKeywordArgs blanks bare type-keyword call args, spares valid look-alikes', async () => {
+    const { blankCTypeKeywordArgs } = await import('../src/extraction/languages/c-cpp');
+    const src = [
+      'void j(void *head, void *map) {',
+      '\tvoid *p = kzalloc_obj(struct bpf_mount_opts);',
+      '\tvoid *e = list_first_entry(head,',
+      '\t\t\tstruct async_entry, domain_list);',
+      '\tvoid *n = hlist_entry_safe(rcu_dereference_raw(hlist_next_rcu(head)),',
+      '\t\t\tstruct bpf_dtab_netdev, index_hlist);',
+      '\treturn container_of(map, struct bpf_map, inner);',
+      '}',
+      'DEFINE_PER_CPU(struct task_struct *, ksoftirqd);',
+      '',
+    ].join('\n');
+    const out = blankCTypeKeywordArgs(src);
+    expect(out.length).toBe(src.length);
+    expect(out).not.toContain('struct bpf_mount_opts');
+    expect(out).toContain('       bpf_mount_opts'); // keyword → spaces, ident stays
+    expect(out).toContain('       async_entry, domain_list');
+    expect(out).toContain('       bpf_dtab_netdev'); // nested-paren predecessor arg
+    expect(out).toContain('       bpf_map, inner'); // `return` precedes real calls
+    // Pointer form blanks the stars too — two plain identifier args remain.
+    expect(out).toContain('       task_struct  , ksoftirqd');
+    // Valid look-alikes stay byte-identical.
+    for (const valid of [
+      'int a = sizeof(struct point);',
+      'int b = offsetof(struct point, y);',
+      'int c = _Generic(x, struct foo *: 1, default: 0);',
+      'int wf(struct a,\n\tstruct b);',
+      'void cast(void *p) { use((struct foo *)p); }',
+      'struct ops { int (*probe)(struct device *dev); };',
+    ]) {
+      expect(blankCTypeKeywordArgs(valid)).toBe(valid);
+    }
+  });
+
+  it('blankCFileScopePrefixedDeclMacros blanks static/extern CAPS-macro lines at any scope', async () => {
+    const { blankCFileScopePrefixedDeclMacros } = await import('../src/extraction/languages/c-cpp');
+    const src = [
+      'static DEFINE_PER_CPU(struct llist_head, rstat_backlog_list);',
+      'void f(void) {',
+      '\tstatic DEFINE_RATELIMIT_STATE(ratelimit, 5 * HZ, 5);',
+      '}',
+      'EXPORT_SYMBOL(vmalloc);',
+      'static DEFINE_PER_CPU(struct cpuhp_cpu_state, cpuhp_state) = {',
+      '',
+    ].join('\n');
+    const out = blankCFileScopePrefixedDeclMacros(src);
+    expect(out.length).toBe(src.length);
+    expect(out).not.toContain('DEFINE_PER_CPU(struct llist_head');
+    expect(out).not.toContain('DEFINE_RATELIMIT_STATE'); // block scope blanks too
+    // Bare CAPS lines parse natively as K&R declarations — untouched.
+    expect(out).toContain('EXPORT_SYMBOL(vmalloc);');
+    // Initializer forms belong to the rewrite, not the blank.
+    expect(out).toContain('cpuhp_state) = {');
+  });
+
+  it('rewriteCPrefixedDeclMacroInitializers rewrites `static CAPS(type, name) = {` into the declaration', async () => {
+    const { rewriteCPrefixedDeclMacroInitializers } = await import('../src/extraction/languages/c-cpp');
+    const line = 'static DEFINE_PER_CPU(struct cpuhp_cpu_state, cpuhp_state) = {';
+    const src = [line, '\t.fail = CPUHP_INVALID,', '};', ''].join('\n');
+    const out = rewriteCPrefixedDeclMacroInitializers(src);
+    expect(out.length).toBe(src.length);
+    const rewritten = out.split('\n')[0] as string;
+    expect(rewritten).toContain('static struct cpuhp_cpu_state');
+    expect(rewritten).not.toContain('DEFINE_PER_CPU');
+    // The NAME keeps its exact original column, and the tail its offsets.
+    expect(rewritten.indexOf('cpuhp_state')).not.toBe(-1);
+    expect(rewritten.indexOf('cpuhp_state', 30)).toBe(line.indexOf('cpuhp_state', 30));
+    expect(rewritten.indexOf('= {')).toBe(line.indexOf('= {'));
+    // Three-argument macros never match.
+    const threeArg = 'static DEFINE_TIMER(t, fn, 0) = {\n};\n';
+    expect(rewriteCPrefixedDeclMacroInitializers(threeArg)).toBe(threeArg);
+  });
+
+  it('blankCVaArgQualifiedTypeArgs blanks multi-token va_arg types, spares single tokens', async () => {
+    const { blankCVaArgQualifiedTypeArgs } = await import('../src/extraction/languages/c-cpp');
+    const src = 'void f(va_list ap) {\n\tconst char *s = va_arg(ap, const char *);\n\tint n = va_arg(ap, int);\n}\n';
+    const out = blankCVaArgQualifiedTypeArgs(src);
+    expect(out.length).toBe(src.length);
+    expect(out).toContain('va_arg(ap              );');
+    expect(out).toContain('va_arg(ap, int);'); // parses natively — untouched
+  });
+
+  it('blankCNamedVariadicDefineDots blanks only the dots of GNU named-variadic params', async () => {
+    const { blankCNamedVariadicDefineDots } = await import('../src/extraction/languages/c-cpp');
+    const named = '#define verbose(env, fmt, args...) log_write(env, fmt, ##args)\nint x;\n';
+    const out = blankCNamedVariadicDefineDots(named);
+    expect(out.length).toBe(named.length);
+    expect(out).toContain('args   )'); // dots → spaces
+    expect(out).toContain('##args'); // body untouched
+    const std = '#define pr(fmt, ...) printk(fmt, __VA_ARGS__)\nint y;\n';
+    expect(blankCNamedVariadicDefineDots(std)).toBe(std);
+  });
+
+  it('blankCSandwichedAnnotations and blankCAutoInference: sandwich and C23-auto guards', async () => {
+    const { blankCSandwichedAnnotations, blankCAutoInference } = await import(
+      '../src/extraction/languages/c-cpp'
+    );
+    const src = 'static notrace void tick_do(void) { }\nstatic nokprobe_inline void arm(void) { }\n';
+    const out = blankCSandwichedAnnotations(src);
+    expect(out.length).toBe(src.length);
+    expect(out).not.toContain('notrace');
+    expect(out).not.toContain('nokprobe_inline');
+    // As a variable name (no following word) it survives.
+    const varUse = 'void f(void) { int notrace = 1; use(notrace); }\n';
+    expect(blankCSandwichedAnnotations(varUse)).toBe(varUse);
+    const c23 = 'void q(void) { auto hb = get_hb(); }\n';
+    const autoOut = blankCAutoInference(c23);
+    expect(autoOut.length).toBe(c23.length);
+    expect(autoOut).toContain('     hb = get_hb();');
+    // The storage-class reading has a TYPE after `auto` — untouched.
+    const storage = 'void s(void) { auto int x = 1; }\n';
+    expect(blankCAutoInference(storage)).toBe(storage);
+  });
+
+  it('blankCStatementMacroCalls spans wrapped iterator macros, spares wrapped real calls', async () => {
+    const { blankCStatementMacroCalls } = await import('../src/extraction/languages/c-cpp');
+    const src = [
+      'static void walk(void *head) {',
+      '\thlist_for_each_entry_rcu(p, head, hlist,',
+      '\t\t\t\t lockdep_is_held(&kprobe_mutex)) {',
+      '\t\tuse(p);',
+      '\t}',
+      '}',
+      '',
+    ].join('\n');
+    const out = blankCStatementMacroCalls(src);
+    expect(out.length).toBe(src.length);
+    expect(out).not.toContain('hlist_for_each_entry_rcu');
+    expect(out).not.toContain('lockdep_is_held');
+    expect(out).toContain('use(p);');
+    // A wrapped REAL call ends in `;` — untouched.
+    const call = 'void f(void) {\n\tdo_thing(a,\n\t\t b);\n}\n';
+    expect(blankCStatementMacroCalls(call)).toBe(call);
+    // A wrapped condition is keyword-led — untouched.
+    const cond = 'void g(int a) {\n\tif (check(a,\n\t\t  a)) {\n\t\tuse(a);\n\t}\n}\n';
+    expect(blankCStatementMacroCalls(cond)).toBe(cond);
+  });
+
   it('restoreDirectiveLines keeps #define lines out of the blanking blast radius', async () => {
     const { extractFromSource } = await import('../src/extraction');
     // FMT_API matches the _API-suffix member blank; without the directive

+ 70 - 0
__tests__/fixtures/kernel-parity/torture.c

@@ -82,3 +82,73 @@ static void spawn_workers(void) {
   register_handler(cb_a);
   signal_connect(&cb_b);
 }
+
+/* ---- deferral round 2 (the linux-idiom preParse family) --------------------
+ * Every shape below used to drop the WHOLE file into error recovery (and
+ * therefore defer it to wasm). Each now parses via a round-2 blank/rewrite;
+ * this section pins kernel-vs-wasm parity on all of them at once. */
+
+/* file-scope prefixed declaration macros — whole-line blank */
+static DEFINE_PER_CPU(struct llist_head, rstat_backlog_list);
+static DECLARE_WORK(init_free_wq, do_free_init);
+extern DECLARE_PER_CPU(struct tick_device, tick_cpu_device);
+
+/* initialized per-cpu declaration — the REWRITE (type + name survive) */
+static DEFINE_PER_CPU(struct conn, cpuhp_state) = {
+  .fd = 1,
+};
+
+/* type-keyword arguments — keyword (and trailing stars) blank */
+static void type_args(void *head, void *map) {
+  void *opts = kzalloc_obj(struct conn);
+  void *entry = list_first_entry(head,
+             struct conn, fd);
+  void *outer = hlist_entry_safe(rcu_dereference_raw(hlist_next_rcu(head)),
+             struct conn, fd);
+  use_ptr(outer, container_of(map, struct conn, fd));
+  use_ptr(opts, entry);
+}
+DEFINE_PER_CPU(struct conn *, ksoftirqd);
+
+/* parameterized annotations — name+args blank whole */
+static void cleanup_scope(void) {
+  struct conn *token __free(kfree) = NULL;
+  use_ptr(token, token);
+}
+static void __printf(1, 2) log_fmt(const char *fmt, ...);
+struct flex_tail {
+  int count;
+  int owners[] __counted_by(count);
+};
+
+/* sandwiched lowercase annotations + C23 auto */
+static notrace void tick_do(int x) { use_val(x); }
+static nokprobe_inline void arm_probe(void) { }
+static void auto_user(void) {
+  auto hb = shadowed_reader();
+  use_val(hb);
+}
+
+/* va_arg with a qualified type argument */
+static void drain_args(va_list ap) {
+  const char *s = va_arg(ap, const char *);
+  int n = va_arg(ap, int);
+  use_ptr((void *)s, (void *)(long)n);
+}
+
+/* multi-line statement-position iterator macro */
+static void walk_rcu(void *head) {
+  hlist_for_each_entry_rcu(pos, head, hlist,
+         lockdep_is_held(&probe_mutex)) {
+    use_ptr(pos, head);
+  }
+}
+
+/* GNU named-variadic define — dots blank, body survives */
+#define verbose(env, fmt, args...) log_writer(env, fmt, ##args)
+
+/* block-scope prefixed declaration macro */
+static void ratelimited_warn(void) {
+  static DEFINE_RATELIMIT_STATE(ratelimit, 5 * HZ, 5);
+  use_ptr(&ratelimit, 0);
+}

+ 48 - 1
docs/design/ccpp-kernel-port-checklist.md

@@ -35,7 +35,11 @@ and the raw bulk path), so no blanking ported to Rust.
   starting points; linux `kernel/`+`mm/` subtrees 79% → 58%), each
   offset-preserving, TS-side, shared by both arms, and a graph-quality win
   for the wasm path itself (git 7.1k → 13.3k nodes; the linux subtrees
-  2.4k → 7.2k): `#ifdef __cplusplus` guard bodies, lone macro lines
+  2.4k → 7.2k — NOTE these are parity-sweep compared-file totals, NOT
+  full-graph counts: round 2 established that full-graph node deltas are
+  small because wasm error recovery was already salvaging most SYMBOLS on
+  deferred files; the blanks' full-graph win is EDGES + phantom cleanup —
+  see the round-2 record below): `#ifdef __cplusplus` guard bodies, lone macro lines
   (`FMT_BEGIN_NAMESPACE`, `Q_OBJECT`), C statement iterator macros
   (`list_for_each_entry(…) { }` and brace-less bodies), C trailing param
   attrs (`int argc UNUSED`), the curated Linux/sparse annotation list
@@ -62,6 +66,49 @@ and the raw bulk path), so no blanking ported to Rust.
   pipe at GB scale dies with ENOBUFS, both of which silently hash truncated
   output as the empty stream.)
 
+- **Deferral round 2 (2026-07-18)** — eight new C-only passes + word-list
+  extensions took the linux `kernel/`+`mm/` deferral **58.6% → 33.9%**
+  (483 → 279 of 824 files; census-driven: `bucket-defers.mjs` clusters each
+  deferring file's FIRST post-preParse error-line shape). New passes, all in
+  `preParseCSource`: parameterized-annotation whole-blank (`__free(kfree)`,
+  `__printf(4,0)`, `__counted_by(n)`, `__bpf_md_ptr(…)` — extends through a
+  stranded field `;`, since a lone-`;` field errors but an empty struct body
+  doesn't), type-keyword-arg scanner (`kzalloc_obj(struct T)`,
+  `list_entry(p, struct T, m)` — a bounded hand scanner, NOT a nested regex
+  (backtracking risk on untrusted repos); head exclusions
+  sizeof/offsetof/`_Generic`/va_arg + call-vs-declaration guard: an
+  identifier or `*` before the head rejects, `return` excepted; blanks
+  trailing stars so `DEFINE_PER_CPU(struct T *, x)` leaves two plain
+  idents), `static|extern CAPS_MACRO(…);` whole-line blank at ANY scope
+  (bare `EXPORT_SYMBOL(x);` parses natively as a K&R decl — probed — and
+  stays), the ONE REWRITE in the family — `static CAPS(type, name) = {` →
+  `static <type> <name> = {` (name + tail keep exact offsets via d-flag
+  indices; blanking would strand the brace block or discard initializer
+  fn-refs), `va_arg(ap, const char *)` second-arg blank (single-token
+  `va_arg(ap, int)` parses natively — probed), GNU named-variadic
+  `#define f(args...)` DOTS-only blank (post-`restoreDirectiveLines` by
+  design; whole-tail blanking fails — `#define NAME` + trailing spaces
+  errors, measured), storage-sandwiched lowercase markers
+  (`static notrace void` — the sandwich is the guard), C23 `auto`
+  (`auto x =` only; `auto int x` untouched), and multi-line spans for the
+  statement-iterator-macro blank (`hlist_for_each_entry_rcu(…,\n
+  lockdep_is_held(&m)) {` — bails on `;`/braces mid-span). Word list +=
+  cacheline family (2- AND 4-underscore spellings), `__noclone`,
+  `__lockfunc`, `__ref`, `__private`, `__bitwise`, `__nosavedata`,
+  `__no_kcsan`, `__cpuidle`, `__ksym`, `__net_initdata`,
+  `__initdata_memblock`/`_or_meminfo`. Cross-repo: git 16.1 → 12.2%,
+  redis 25.3 → 24.1%, fmt/protobuf unchanged (cpp-dominant — C-only passes,
+  correct), **0 diffs all five sweeps**. Linux full-tree (2c gate runs):
+  kernel-arm parse **356 → 306s**, envelope 19.1 → ~17.1min (host-
+  contaminated, indicative), counts **2,049,153 / 6,413,518** (+858 nodes,
+  **+6,585 edges** vs R7a — the SYMBOLS were mostly already error-recovered;
+  the graph win is relationships + phantom cleanup). Deliberately skipped:
+  `#ifdef CONFIG_X` if/else interleaves + labels (genuine preprocessing),
+  TP_PROTO/TRACE_EVENT DSL headers (no real code to recover), `module_init(x)`
+  without `;` (K&R-definition ambiguity), single-token va_arg (native).
+  Torture fixture grew a round-2 section (both-arm parity pinned); unit
+  tests in extraction.test.ts; suite 2517 green.
+
 The sections below are §0a-recipe step 1's output — every TS-side branch the
 walker mirrors, with file:line anchors (as of `705e501`). Read WITH
 `docs/design/rust-kernel-migration-plan.md` (§0a recipe, §5 gates).

+ 43 - 6
docs/design/rust-kernel-migration-plan.md

@@ -66,12 +66,29 @@ them are the ORIGINAL plan and carry expectations that measurement later correct
       envelope now **19.1min on a substantially RICHER graph** (the new
       preParse blanks recover previously-error-swallowed code; wasm-arm on
       the same graph is 22.9min — the 17.6 record was the old smaller graph
-      and isn't directly comparable). The <10min-on-8c target remains open;
-      levers left, ranked: **C/C++ deferral cuts (58% of linux files still
-      defer to wasm — each recovered idiom moves parse toward the native
-      floor)** > backpressure ~120s (checkpoint I/O floor) > E-scan/settle/
-      read-mapping (~70–90s each, approaching honest work) > the 8c re-run
-      formality.
+      and isn't directly comparable). 8c re-run DONE post-R7a (§7a.5):
+      **16.4min** (pre-R7a record 18.3min), EXIT 0, counts == both 2c arms,
+      WAL 1.34GB. The <10min-on-8c target remains open, and the re-run
+      re-ranked the levers honestly: at 8c the parse-loop (202.6s) is
+      already AT the single-writer floor, so **the target gap is ~entirely
+      the core-invariant resolution superphase (715s ≈ 12 of the 16.4min)
+      — the per-ref path is THE 8c lever**. C/C++ deferral round 2 DONE
+      2026-07-18 (full record: checklist doc): eight new C-only preParse
+      passes + word-list extensions took kernel/+mm/ deferral
+      **58.6% → 33.9%** (git 16.1 → 12.2%, redis 25.3 → 24.1%,
+      fmt/protobuf unchanged — cpp-dominant, correct no-op), five-repo
+      sweeps 0-diff, linux full-tree both arms **2,049,153 / 6,413,518**
+      with **byte-identical dumps** (10,446,478 lines, sha `6dd1185b…`);
+      kernel-arm parse-loop **356 → 306s** at 2c, envelope ~17.1min
+      (host-contaminated, indicative). Honesty note: full-graph node
+      deltas are small (+858) — wasm error recovery was already salvaging
+      most SYMBOLS on deferred files; the real win is EDGES (+6,585),
+      phantom cleanup, and native-path coverage. Remaining deferral is
+      policy-skips (CONFIG interleaves, TP_PROTO DSL, module_init-no-semi)
+      + small buckets — this lever is largely SPENT. Queue now: **per-ref
+      resolution path** (the core-invariant superphase) > backpressure
+      ~120s (checkpoint I/O floor) > E-scan/settle/read-mapping (~70–90s
+      each, approaching honest work).
 - [x] **R7a. C/C++ port** — DONE 2026-07-17, same-day walker+gates after the
       survey (#1344) and grammar vendoring (#1345). One dual-language walker
       (`codegraph-kernel/src/ccpp/`), preParse HOISTED to the route point
@@ -741,6 +758,26 @@ algorithmic wins only.
 rock) > backpressure ~120s (checkpoint I/O floor) > E-scan 69–93s (approaching
 honest regex work over 1.5GB) > settle 88s > read-mapping 57s.
 
+#### 7a.5 8-core re-run, post-R7a (2026-07-17) — 16.4min; the 8c gap is now all resolution
+
+Same provisioning as the §7a.2 retry (cg1212 at cpuset 0-7 / 7GB), the deployed
+R7a build, fresh init of the v7.2-rc2 tree: **EXIT 0, envelope 981s = 16.4min**
+(pre-R7a 8c record: 18.3min — and that was the smaller pre-blank graph).
+Counts **2,048,295 / 6,406,933 == both 2c arms**; WAL peak 1.34GB (same
+contained regime as 1.09–1.57GB records). Phases: parse-loop **202.6s**
+(pre-R7a all-wasm 8c: 208.7s — both sit ON the single-writer store floor, so
+8c parse is writer-bound, not extraction-bound) · resolution superphase
+**715.0s** (was 835.9s) containing callback-synthesis **257.4s** (was 338.7s)
+and edge-index-recreate 52.0s · maintenance 47.6s. The −1.9min vs the record
+is the post-#1336 rounds (#1339 countGuard, #1341 cFnPtr, R7a native parse +
+defer-reuse) landing at 8c for the first time.
+
+**Consequence for the <10min target:** ~12 of the 16.4 minutes are the
+core-invariant resolution superphase. Deferral cuts can't materially move the
+8c envelope (parse is already at the writer lane); they remain queued for
+graph richness + the 2c/low-core envelope. The 8c target now lives or dies on
+the per-ref resolution path (§7a.2's lever (a)).
+
 ### 7b. Arc 3 — graph richness (forensics-backed; adopt cbm's real extras, skip inflation)
 Priority order, each gated by the standard A/B + node-explosion probes:
 1. **Test→subject edges** (first-class `tests` edges at index time; we compute covering

+ 449 - 10
src/extraction/languages/c-cpp.ts

@@ -961,14 +961,55 @@ export function blankCStatementMacroCalls(source: string): string {
         }
       }
     }
-    if (close < 0) continue; // parens don't balance on the line
-    const after = line.slice(close + 1).replace(/\r$/, '').trim();
+    let endLine = i;
+    if (close < 0) {
+      // Parens don't balance on the head line — the kernel wraps iterator
+      // macros (`hlist_for_each_entry_rcu(p, head, hlist,\n\t\t
+      // lockdep_is_held(&kprobe_mutex)) {`). Continue the same
+      // string-skipping paren scan over a few continuation lines. A `;` or
+      // a brace anywhere in the span means a real statement or compound
+      // literal — bail (missing a blank is safe; corrupting one is not).
+      if (line.indexOf(';') !== -1) continue;
+      for (let j = i + 1; j <= i + 5 && j < lines.length && close < 0; j++) {
+        const cont = lines[j] as string;
+        let bail = false;
+        for (let k = 0; k < cont.length; k++) {
+          const ch = cont[k];
+          if (ch === '"' || ch === "'") {
+            const quote = ch;
+            k++;
+            while (k < cont.length && cont[k] !== quote) {
+              if (cont[k] === '\\') k++;
+              k++;
+            }
+            continue;
+          }
+          if (ch === ';' || ch === '{' || ch === '}') {
+            bail = true;
+            break;
+          }
+          if (ch === '(') depth++;
+          else if (ch === ')') {
+            depth--;
+            if (depth === 0) {
+              close = k;
+              endLine = j;
+              break;
+            }
+          }
+        }
+        if (bail) break;
+      }
+      if (close < 0) continue;
+    }
+    const endLineStr = lines[endLine] as string;
+    const after = endLineStr.slice(close + 1).replace(/\r$/, '').trim();
     if (after === '') {
       // Brace on the next line (`ql_foreach(…)\n{`) or a brace-less
       // single-statement body (`for_each_subsys(ss, i)\n\tstmt;` — blanking
       // leaves the bare statement, valid C). A next line starting with an
       // operator/string/`;` is an expression continuation — bail.
-      let next = i + 1;
+      let next = endLine + 1;
       while (next < lines.length && content(lines[next] as string) === '') next++;
       if (next >= lines.length) continue;
       const first = content(lines[next] as string)[0];
@@ -977,15 +1018,69 @@ export function blankCStatementMacroCalls(source: string): string {
       continue;
     }
     const identStart = line.indexOf(m[1] as string);
-    lines[i] =
-      line.slice(0, identStart) +
-      ' '.repeat(close + 1 - identStart) +
-      line.slice(close + 1);
+    if (endLine === i) {
+      lines[i] =
+        line.slice(0, identStart) +
+        ' '.repeat(close + 1 - identStart) +
+        line.slice(close + 1);
+    } else {
+      lines[i] = line.slice(0, identStart) + line.slice(identStart).replace(/[^\r]/g, ' ');
+      for (let j = i + 1; j < endLine; j++) {
+        lines[j] = (lines[j] as string).replace(/[^\r]/g, ' ');
+      }
+      lines[endLine] =
+        endLineStr.slice(0, close + 1).replace(/[^\r]/g, ' ') + endLineStr.slice(close + 1);
+      i = endLine; // the blanked span can't host another head
+    }
     changed = true;
   }
   return changed ? lines.join('\n') : source;
 }
 
+/**
+ * Blank a lowercase compiler-annotation word SANDWICHED between a storage
+ * class and the rest of a declaration — `static notrace void tick(…)`,
+ * `static nokprobe_inline void arm(…)` (kernel compiler.h markers). The
+ * dunder word-list can't carry these bare-word forms: `notrace` is a
+ * plausible identifier. The sandwich IS the guard — the token counts only
+ * when directly preceded by `static`/`extern`/`inline` AND followed by
+ * another word, a position where it cannot be a variable name (an archaic
+ * implicit-int `static notrace = 1;` fails the following-word requirement).
+ * C-only.
+ */
+const C_SANDWICHED_ANNOTATIONS = [
+  'noinline_for_stack', 'nokprobe_inline', 'noinline', 'notrace', 'noinstr',
+] as const;
+const C_SANDWICH_RE = new RegExp(
+  `\\b(static|extern|inline)([ \\t]+)(${C_SANDWICHED_ANNOTATIONS.join('|')})\\b(?=[ \\t]+[A-Za-z_])`,
+  'g'
+);
+export function blankCSandwichedAnnotations(source: string): string {
+  C_SANDWICH_RE.lastIndex = 0;
+  if (!C_SANDWICH_RE.test(source)) return source;
+  C_SANDWICH_RE.lastIndex = 0;
+  return source.replace(
+    C_SANDWICH_RE,
+    (_m, storage: string, ws: string, ann: string) => storage + ws + ' '.repeat(ann.length)
+  );
+}
+
+/**
+ * Blank a C23 `auto` type-inference keyword — `auto hb = hbr.hb;` (the
+ * futex code; tree-sitter-c predates C23 auto and errors the enclosing
+ * function). The old storage-class reading (`auto int x = 1;`) has a TYPE
+ * between `auto` and the `=` and is untouched — the match requires
+ * `auto IDENT =` directly, which only the C23 form exhibits. Blanking
+ * leaves a plain assignment statement. C-only.
+ */
+const C_AUTO_INFER_RE = /\bauto(?=[ \t]+[A-Za-z_]\w*[ \t]*=)/g;
+export function blankCAutoInference(source: string): string {
+  C_AUTO_INFER_RE.lastIndex = 0;
+  if (!C_AUTO_INFER_RE.test(source)) return source;
+  C_AUTO_INFER_RE.lastIndex = 0;
+  return source.replace(C_AUTO_INFER_RE, () => '    ');
+}
+
 /**
  * Blank a trailing parameter-attribute macro — `int argc UNUSED,` /
  * `struct repository *repo UNUSED)` — git's house style for
@@ -1035,13 +1130,23 @@ export function blankCTrailingParamAttrMacros(source: string): string {
 const C_KERNEL_ANNOTATIONS = [
   '__init', '__exit', '__initdata', '__initconst', '__exitdata',
   '__devinit', '__devexit', '__cpuinit', '__meminit', '__meminitdata',
-  '__net_init', '__net_exit', '__init_or_module',
+  '__net_init', '__net_exit', '__net_initdata', '__init_or_module',
   '__user', '__kernel', '__iomem', '__percpu', '__rcu', '__force', '__nocast',
   '__must_check', '__maybe_unused', '__always_unused', '__used', '__cold',
   '__hot', '__weak', '__pure', '__sched', '__malloc', '__visible',
   '__deprecated', '__ro_after_init', '__read_mostly', '__refdata',
   '__latent_entropy', '__randomize_layout', '__no_randomize_layout',
   '__bpf_kfunc', '__function_aligned', '__always_inline', '__noreturn',
+  // Round-2 additions, each measured heading first-error lines on the
+  // v7.2-rc2 kernel/+mm/ census (2026-07-17): cacheline placement, sparse
+  // lock/section markers, and bpf/typecheck annotations. Real dunder TYPES
+  // (`__u32`, `__s64`) and operators (`__alignof__`) are deliberately absent.
+  '__cacheline_aligned_in_smp', '__cacheline_aligned',
+  '__cacheline_internodealigned_in_smp', '____cacheline_aligned_in_smp',
+  '____cacheline_aligned', '____cacheline_internodealigned_in_smp',
+  '__noclone', '__lockfunc', '__ref', '__private', '__bitwise',
+  '__nosavedata', '__no_kcsan', '__cpuidle', '__ksym',
+  '__initdata_memblock', '__initdata_or_meminfo',
 ] as const;
 // `(?!\s*\()` keeps the parameterized annotations (`__printf(1, 2)`,
 // `__aligned(8)`, `__section("x")`) intact — blanking just their name would
@@ -1070,6 +1175,327 @@ export function blankCKernelAnnotations(source: string): string {
   return out;
 }
 
+/**
+ * Blank the PARAMETERIZED sparse/compiler annotations whole — name AND
+ * argument list — `struct file *f __free(fput) = NULL;`, `static void
+ * __printf(4, 0) log_it(…)`, `} owners[] __counted_by(count);`,
+ * `__bpf_md_ptr(struct bpf_iter_meta *, meta);`. The word-list blank above
+ * deliberately skips these via `(?!\s*\()` because blanking just the NAME
+ * strands `(args)` as a floating parenthesis — so every such file kept
+ * deferring (the v7.2-rc2 kernel/+mm/ census puts `__free`/`__printf`/
+ * `__counted_by`/`__aligned` at the head of 39 first-error lines). Blanking
+ * the whole `__name(args)` span leaves an ordinary declaration.
+ *
+ * CURATED for the same reason as the word list: `__aligned(8)` (annotation)
+ * and `__hash(key)` (a real static helper call) are byte-shape identical, so
+ * only reserved-namespace names that are annotations in every codebase that
+ * spells them are listed. One nesting level of parens is allowed
+ * (`__aligned(sizeof(struct x))`); newlines inside the span survive so byte
+ * offsets are preserved. C-only.
+ */
+const C_PARAMETERIZED_ANNOTATIONS = [
+  '__free', '__printf', '__scanf', '__counted_by', '__counted_by_le',
+  '__counted_by_be', '__guarded_by', '__pt_guarded_by', '__acquires',
+  '__releases', '__must_hold', '__cleanup', '__aligned', '__section',
+  '__bpf_md_ptr', '__assume_aligned',
+] as const;
+const C_PARAM_ANNOTATION_RE = new RegExp(
+  `\\b(${[...C_PARAMETERIZED_ANNOTATIONS].sort((a, b) => b.length - a.length).join('|')})[ \\t]*\\((?:[^()]|\\([^()]*\\))*\\)`,
+  'g'
+);
+export function blankCParameterizedAnnotationMacros(source: string): string {
+  if (source.indexOf('__') === -1) return source;
+  C_PARAM_ANNOTATION_RE.lastIndex = 0;
+  if (!C_PARAM_ANNOTATION_RE.test(source)) return source;
+  C_PARAM_ANNOTATION_RE.lastIndex = 0;
+  let result = '';
+  let last = 0;
+  let m: RegExpExecArray | null;
+  while ((m = C_PARAM_ANNOTATION_RE.exec(source)) !== null) {
+    const start = m.index;
+    let end = start + (m[0] as string).length;
+    // Field-position form — `__bpf_md_ptr(struct bpf_iter_meta *, meta);` as
+    // the whole line: a lone `;` field is ITSELF a parse error (measured;
+    // an empty struct body is fine), so the semicolon blanks with it. A
+    // mid-line match (`… __free(fput) = NULL;`) keeps its statement tail.
+    const lineStart = source.lastIndexOf('\n', start - 1) + 1;
+    if (/^[ \t]*$/.test(source.slice(lineStart, start))) {
+      const after = /^[ \t]*;/.exec(source.slice(end));
+      if (after) end += after[0].length;
+    }
+    result += source.slice(last, start) + source.slice(start, end).replace(/[^\n\r]/g, ' ');
+    last = end;
+  }
+  return result + source.slice(last);
+}
+
+/**
+ * Blank a bare `struct`/`union`/`enum` TYPE-keyword argument inside a macro
+ * call — `kzalloc_obj(struct bpf_mount_opts)`, `alloc_percpu(struct irqstat)`,
+ * `list_first_entry(&pending,\n\t\tstruct async_entry, domain_list)` — the
+ * `container_of` disease generalized (that blank stays as the keyed
+ * precedent). A bare type is never a valid C expression argument, so
+ * tree-sitter drops into error recovery at every such call; removing just the
+ * keyword leaves a plain identifier argument the grammar parses natively.
+ * This single shape heads ~35 first-error lines on the kernel/+mm/ census
+ * (kzalloc_obj/list_entry/alloc_percpu + multi-line continuations).
+ *
+ * Guarded three ways, because `f(struct T)` has VALID look-alikes:
+ *  - head exclusions: `sizeof(struct T)` / `offsetof(struct T, m)` /
+ *    `_Generic(x, struct T *: …)` all parse natively (probed) and must keep
+ *    their keyword; `va_arg` gets its own dedicated blank below.
+ *  - call-vs-declaration: in `int wf(struct a, struct b);` (a wrapped
+ *    prototype — valid C) the head is preceded by a TYPE, so any identifier
+ *    or `*` before the head rejects the match — except the word `return`,
+ *    which precedes real calls. Newline/`=`/`,`/`(`/`;`/`{`/`>` etc. accept.
+ *  - the keyword must OPEN a top-level argument and the argument must be
+ *    exactly `struct T` (optionally `struct T *`, whose stars blank too —
+ *    `DEFINE_PER_CPU(struct task_struct *, ksoftirqd)` leaves two plain
+ *    identifier arguments): a cast (`f((struct T *)p)`) opens a nested
+ *    group, and a declaration argument (`TP_PROTO(struct foo *bar)`) trails
+ *    an extra identifier — neither matches.
+ * A hand-rolled bounded scanner rather than a nested-alternation regex:
+ * preceding args may themselves contain calls
+ * (`hlist_entry_safe(rcu_dereference_raw(hlist_next_rcu(&d->h)),\n
+ * struct bpf_dtab_netdev, index_hlist)`), and lazy nested regex groups over
+ * untrusted repo text invite catastrophic backtracking. C-only.
+ */
+const C_TYPE_ARG_HEAD_EXCLUSIONS = new Set([
+  'sizeof', 'alignof', '_Alignof', 'typeof', '__typeof__', '__typeof',
+  'offsetof', '_Generic', 'va_arg', 'if', 'while', 'for', 'switch', 'case',
+]);
+const C_TYPE_ARG_HEAD_RE = /\b([A-Za-z_]\w*)[ \t]*\(/g;
+const C_TYPE_ARG_OPENER_RE = /^(struct|union|enum)([ \t\r\n]+)([A-Za-z_]\w*)([ \t\r\n]*\*+)?(?=[ \t\r\n]*[,)])/;
+const C_TYPE_ARG_SCAN_CAP = 600;
+export function blankCTypeKeywordArgs(source: string): string {
+  if (!/\b(?:struct|union|enum)[ \t\r\n]/.test(source)) return source;
+  let chars: string[] | null = null;
+  C_TYPE_ARG_HEAD_RE.lastIndex = 0;
+  let m: RegExpExecArray | null;
+  while ((m = C_TYPE_ARG_HEAD_RE.exec(source)) !== null) {
+    const head = m[1] as string;
+    if (C_TYPE_ARG_HEAD_EXCLUSIONS.has(head)) continue;
+    // Reject declaration shapes: an identifier or `*` (a return/field type)
+    // immediately before the head — unless that word is `return`.
+    let j = m.index - 1;
+    while (j >= 0 && (source[j] === ' ' || source[j] === '\t')) j--;
+    const prev = j >= 0 ? (source[j] as string) : '';
+    if (/[\w*]/.test(prev)) {
+      let w = j;
+      while (w >= 0 && /\w/.test(source[w] as string)) w--;
+      if (source.slice(w + 1, j + 1) !== 'return') continue;
+    }
+    const open = m.index + (m[0] as string).length - 1;
+    let depth = 1;
+    let atArgStart = true;
+    for (let k = open + 1; k < source.length && k - open <= C_TYPE_ARG_SCAN_CAP; k++) {
+      const ch = source[k] as string;
+      if (ch === '"' || ch === "'") {
+        const quote = ch;
+        k++;
+        while (k < source.length && source[k] !== quote) {
+          if (source[k] === '\\') k++;
+          k++;
+        }
+        atArgStart = false;
+        continue;
+      }
+      if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') continue;
+      if (ch === '(') {
+        depth++;
+        atArgStart = false;
+        continue;
+      }
+      if (ch === ')') {
+        depth--;
+        if (depth === 0) break;
+        atArgStart = false;
+        continue;
+      }
+      if (ch === ';' || ch === '{' || ch === '}') break; // not an argument list
+      if (ch === ',') {
+        if (depth === 1) atArgStart = true;
+        continue;
+      }
+      if (depth === 1 && atArgStart) {
+        const opener = C_TYPE_ARG_OPENER_RE.exec(source.slice(k, k + 160));
+        if (opener) {
+          chars ??= [...source];
+          const kw = opener[1] as string;
+          for (let b = k; b < k + kw.length; b++) chars[b] = ' ';
+          if (opener[4]) {
+            const tail = opener[4] as string;
+            const stars = (/\*+$/.exec(tail) as RegExpExecArray)[0].length;
+            const starsStart =
+              k + kw.length + (opener[2] as string).length + (opener[3] as string).length + (tail.length - stars);
+            for (let b = starsStart; b < starsStart + stars; b++) chars[b] = ' ';
+          }
+          k += (opener[0] as string).length - 1;
+        }
+      }
+      atArgStart = false;
+    }
+  }
+  return chars ? chars.join('') : source;
+}
+
+/**
+ * Blank a file-scope `static`/`extern`-prefixed ALL-CAPS declaration macro —
+ * `static DEFINE_PER_CPU(struct llist_head, rstat_backlog_list);`,
+ * `static DECLARE_WORK(init_free_wq, do_free_init);`,
+ * `static DEFINE_RATELIMIT_STATE(ratelimit, 5 * HZ, 5);` — the single
+ * largest deferral family on the kernel/+mm/ census (~45 first-error lines
+ * across the DEFINE_/DECLARE_ variants). A storage-class specifier followed
+ * by a call expression is never valid C, so every one errors; the whole line
+ * blanks to spaces (the macro-declared variable was invisible to extraction
+ * anyway, and the file's remaining symbols recover).
+ *
+ * The UNPREFIXED form (`EXPORT_SYMBOL(x);`, `DEFINE_MUTEX(lock);` at column
+ * 0) parses natively as a K&R implicit-int declaration (probed) — extraction
+ * ignores declarations, so those lines are left alone; the type-keyword-arg
+ * blank above already recovers the `DEFINE_PER_CPU(struct T, x);` bare form.
+ * Matched tightly: `static`/`extern` first on the line (indentation allowed —
+ * `static DEFINE_RATELIMIT_STATE(…)` appears at BLOCK scope too, and the
+ * storage-class-plus-call shape is invalid at every scope), CAPS macro name,
+ * parens balancing ON the line (string literals skipped), then exactly `;`
+ * to end of line. Initializer forms (`… ) = { …`) are deliberately not
+ * matched — blanking the head would strand the brace block. C-only.
+ */
+const C_PREFIXED_DECL_MACRO_RE = /^[ \t]*(?:static|extern)[ \t]+[A-Z][A-Z0-9_]{2,}[ \t]*\(/;
+export function blankCFileScopePrefixedDeclMacros(source: string): string {
+  if (!/^[ \t]*(?:static|extern)[ \t]+[A-Z]/m.test(source)) return source;
+  const lines = source.split('\n');
+  let changed = false;
+  for (let i = 0; i < lines.length; i++) {
+    const line = lines[i] as string;
+    const m = C_PREFIXED_DECL_MACRO_RE.exec(line);
+    if (!m) continue;
+    const open = line.indexOf('(', m[0].length - 1);
+    let depth = 0;
+    let close = -1;
+    for (let k = open; k < line.length; k++) {
+      const ch = line[k];
+      if (ch === '"' || ch === "'") {
+        const quote = ch;
+        k++;
+        while (k < line.length && line[k] !== quote) {
+          if (line[k] === '\\') k++;
+          k++;
+        }
+        continue;
+      }
+      if (ch === '(') depth++;
+      else if (ch === ')') {
+        depth--;
+        if (depth === 0) {
+          close = k;
+          break;
+        }
+      }
+    }
+    if (close < 0) continue; // spans lines — leave for a future round
+    if (line.slice(close + 1).replace(/\r$/, '').trim() !== ';') continue;
+    lines[i] = line.replace(/[^\n\r]/g, ' ');
+    changed = true;
+  }
+  return changed ? lines.join('\n') : source;
+}
+
+/**
+ * REWRITE (the one non-blank pass in this family — it moves a token) a
+ * 2-argument `static CAPS_MACRO(type, name) = {`-style initialized
+ * declaration macro into the declaration it expands to — `static
+ * DEFINE_PER_CPU(struct cpuhp_cpu_state, cpuhp_state) = {` becomes `static
+ * struct cpuhp_cpu_state       cpuhp_state) …` → `static struct
+ * cpuhp_cpu_state` + padding + `cpuhp_state` + padding + `= {`. The blank
+ * family can't help here: blanking the head strands the brace block, and
+ * dropping the whole span would discard the initializer's function
+ * references (`.startup.single = bringup_cpu` — real cFnPtr wiring). The
+ * NAME keeps its exact original column and the `= {` tail keeps its exact
+ * offsets (`d`-flag group indices); only the TYPE token sits left of where
+ * the macro name was, and type tokens contribute strings, not positions.
+ * Only the two-argument `(<type-ish>, <ident>)` form with an `=` tail
+ * matches; three-argument macros and `;`-terminated forms (the blank above)
+ * never do. C-only.
+ */
+const C_PREFIXED_DECL_MACRO_INIT_RE = /^([ \t]*)static[ \t]+[A-Z][A-Z0-9_]{2,}[ \t]*\(([^();'"]*?),[ \t]*([A-Za-z_]\w*)[ \t]*\)([ \t]*=[ \t]*\{?[ \t]*\r?)$/d;
+export function rewriteCPrefixedDeclMacroInitializers(source: string): string {
+  if (!/^[ \t]*static[ \t]+[A-Z]/m.test(source)) return source;
+  const lines = source.split('\n');
+  let changed = false;
+  for (let i = 0; i < lines.length; i++) {
+    const line = lines[i] as string;
+    const m = C_PREFIXED_DECL_MACRO_INIT_RE.exec(line);
+    if (!m) continue;
+    const indices = (m as RegExpExecArray & { indices: Array<[number, number]> }).indices;
+    const arg1 = (m[2] as string).trim().replace(/[ \t]+/g, ' ');
+    if (!/^[A-Za-z_][\w \t*]*$/.test(arg1)) continue; // a type-token run only
+    const name = m[3] as string;
+    const nameStart = (indices[3] as [number, number])[0];
+    const tailStart = (indices[4] as [number, number])[0];
+    const prefix = (m[1] as string) + 'static ' + arg1;
+    if (prefix.length + 1 > nameStart) continue; // rewrite must fit left of the name
+    lines[i] =
+      prefix +
+      ' '.repeat(nameStart - prefix.length) +
+      name +
+      ' '.repeat(tailStart - nameStart - name.length) +
+      line.slice(tailStart);
+    changed = true;
+  }
+  return changed ? lines.join('\n') : source;
+}
+
+/**
+ * Blank a QUALIFIED/POINTER type argument of `va_arg` — `va_arg(ap, const
+ * char *)`, `va_arg(args, unsigned long)`. tree-sitter-c parses the
+ * single-token forms (`va_arg(ap, int)`, `va_arg(ap, foo_t)`) natively
+ * (probed), but multi-token type descriptors error the whole enclosing
+ * function. Blanking the comma and the entire second argument leaves
+ * `va_arg(ap              )` — a one-argument call the grammar accepts.
+ * Function-pointer types (`va_arg(ap, void (*)(int))`) contain parens and
+ * are deliberately unmatched. C-only.
+ */
+const C_VA_ARG_RE = /\bva_arg[ \t]*\(([^(),]+)(,[^()]*)\)/g;
+export function blankCVaArgQualifiedTypeArgs(source: string): string {
+  if (source.indexOf('va_arg') === -1) return source;
+  C_VA_ARG_RE.lastIndex = 0;
+  return source.replace(C_VA_ARG_RE, (m, arg1: string, rest: string) => {
+    if (/^,[ \t]*[A-Za-z_]\w*[ \t]*$/.test(rest)) return m; // single token — parses natively
+    return `va_arg(${arg1}${rest.replace(/[^\n\r]/g, ' ')})`;
+  });
+}
+
+/**
+ * Blank the DOTS of a GNU NAMED-variadic function-like `#define` parameter —
+ * `#define verbose(env, fmt, args...) …` → `#define verbose(env, fmt,
+ * args   ) …`. The `ident...` parameter form (unlike standard `...`) errors
+ * the DIRECTIVE itself (measured — and so does trailing whitespace after a
+ * bare `#define NAME`, which is why the tail is NOT blanked wholesale); with
+ * just the dots blanked the params parse as ordinary identifiers and the
+ * body (`##args` included — the preproc body is opaque to the grammar) is
+ * untouched. `restoreDirectiveLines` exists to keep directives out of the
+ * other blanks' blast radius, so this pass runs AFTER the restore and edits
+ * the directive deliberately. Multi-line bodies are fine — the params always
+ * sit on the `#define` line. C-only (the census hits are the kernel's bpf
+ * verifier headers).
+ */
+const C_NAMED_VARIADIC_DEFINE_RE = /^([ \t]*#[ \t]*define[ \t]+[A-Za-z_]\w*\([^()\n]*?\b\w+)\.\.\./;
+export function blankCNamedVariadicDefineDots(source: string): string {
+  if (source.indexOf('...') === -1) return source;
+  const lines = source.split('\n');
+  let changed = false;
+  for (let i = 0; i < lines.length; i++) {
+    const line = lines[i] as string;
+    const m = C_NAMED_VARIADIC_DEFINE_RE.exec(line);
+    if (!m) continue;
+    const keep = m[1] as string;
+    lines[i] = keep + '   ' + line.slice(keep.length + 3);
+    changed = true;
+  }
+  return changed ? lines.join('\n') : source;
+}
+
 /** C source pre-processing: neutralize `#ifdef __cplusplus` compat-guard
  * bodies (invisible to a C compiler; `extern "C" {` otherwise errors every
  * public header), blank declaration-markup macro calls and lone macro lines
@@ -1080,19 +1506,32 @@ export function blankCKernelAnnotations(source: string): string {
  * kernel prototypes in plain `.h`) — the same content-gated CUDA blank as
  * C++. Offset-preserving. */
 function preParseCSource(source: string): string {
+  const inner = blankCKernelAnnotations(blankCCplusplusGuardBodies(source));
   let blanked = blankCLeadingAttrMacros(
     blankLoneMacroLines(
       blankCStatementMacroCalls(
         blankCTrailingParamAttrMacros(
           blankCppAnnotationMacroCalls(
-            blankCKernelAnnotations(blankCCplusplusGuardBodies(source))
+            rewriteCPrefixedDeclMacroInitializers(
+              blankCFileScopePrefixedDeclMacros(
+                blankCVaArgQualifiedTypeArgs(
+                  blankCTypeKeywordArgs(
+                    blankCParameterizedAnnotationMacros(
+                      blankCAutoInference(blankCSandwichedAnnotations(inner))
+                    )
+                  )
+                )
+              )
+            )
           )
         )
       )
     )
   );
   if (looksLikeCudaSource(blanked)) blanked = blankCudaConstructs(blanked);
-  return restoreDirectiveLines(source, blanked);
+  // The named-variadic `#define` pass runs AFTER the directive restore — it
+  // deliberately edits directive lines (see its doc comment).
+  return blankCNamedVariadicDefineDots(restoreDirectiveLines(source, blanked));
 }
 
 export const cppExtractor: LanguageExtractor = {