Files
codegraph/docs/design/ccpp-kernel-port-checklist.md

226 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# C/C++ kernel port (R7a) — the bug-for-bug checklist
**Status: COMPLETE — walker SHIPPED + gates PASSED, c/cpp DEFAULT-ROUTED
(2026-07-17).** Walker: `codegraph-kernel/src/ccpp/mod.rs` (one dual-language
module, every branch below mirrored; its header comment lists the quirks).
Grammars: tree-sitter-c v0.24.2 (`b780e47`, parser.c `f2883ff9…`) +
tree-sitter-cpp v0.23.4 (`f41e1a0`, parser.c `2a35a43b…`, scanner.c
`cf60387d…`), crates pinned `=exact` in Cargo.toml, wasm vendored from the
same tags, kernel-grammar-parity green. preParse HOISTED to the route point
(`preParsedSource` in src/extraction/kernel/index.ts — both tryKernelExtract
and the raw bulk path), so no blanking ported to Rust.
**Gate results (2026-07-17):**
- Parity sweeps — **0 diffs on every compared file**: redis 592, git 790,
fmt 42, protobuf 925, ALS-Community (UE spot-check) 40 files byte-parity.
- Full-init dump-diffs — **byte-identical** kernel-arm vs wasm-arm: redis
(131,921 dump lines), git (160,844), fmt (35,433), protobuf (780,620),
ALS (3,694).
- Torture fixtures torture.c/.cpp/.hpp + CRLF variants + Metal/CUDA
hoist-parity + defer tests in `__tests__/kernel-ccpp-parity.test.ts`; new
preParse blanks unit-tested in extraction.test.ts; full suite green with
`CODEGRAPH_KERNEL_EXPECT=1`.
- **Deferral-rate guard — CORRECTED BY MEASUREMENT** (the §4f pattern): the
<10% bar was calibrated on ts/java/py/go (00.42% parse-error incidence).
Macro-heavy C/C++ genuinely parses with errors at double-digit file rates
(final sweeps: als 9%, git 16.1%, redis 25.3%, protobuf 25.8%, fmt 42% —
fmt's template metaprogramming + `.operator[]`-in-decltype shapes are
grammar-inherent), and every erroring file defers BY POLICY. Measured with
the defer disabled (`CODEGRAPH_KERNEL_CCPP_ERROR_EXTRACT=1`, sweep-only
hatch): recovery-divergence is real (21/207 redis, 8/382 git, 9/31 fmt
erroring files extract differently across UTF-8/UTF-16), so the defer
stays. The sweep harness now takes `--max-deferral` (default 0.1; use 0.5
for c/cpp — a broken walker still trips it by deferring ~everything).
- **Seven NEW/extended preParse blanks** cut real incidence (from 32%/52%
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 — 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
(`__init`/`__user`/… — structural matching is impossible there: `__u32
count` is shape-identical; parameterized `__printf(1,2)` guarded out) +
`container_of`'s type-keyword argument, leading attr macros extended to
cpp, and directive-line restore (stops the older blanks corrupting
`#define` lines).
- **Defer-reuse (the linux-economics fix):** a deferred file used to pay the
pipeline three times — the worker's raw kernel try, extractFromSource's
kernel RE-try, then wasm with a third preParse. A one-slot defer memo in
the route point short-circuits the repeat kernel attempt and hands the
already-blanked source to the wasm fallback (`sourceIsPreParsed`). On
linux this + the annotation blanks took the kernel-arm parse-loop from
560s (WORSE than the 426s wasm arm) to **356s vs 435s wasm-arm (18%)**,
and the 2c/6GB envelope to **19.1 min kernel-arm vs 22.9 min wasm-arm
(17%)** with a RICHER graph (2,048,295 nodes / 6,406,933 edges; two
independent kernel-arm runs byte-same counts).
- **Linux-scale dump gate:** kernel-arm and wasm-arm full graphs are
**byte-identical**`dump-graph.mjs` over both 5.2GB DBs:
10,444,551 dump lines each, sha256 `cd4182e6…` on both. (Dumping a 2M-node
DB needs a big-heap host run — `node --max-old-space-size=16000 …
> file` then hash the FILE; the in-container 6GB heap OOMs and a straight
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).
## Architecture decisions (already made by the plan)
1. **preParse stays TS-side and is HOISTED to the route point.**
`tryKernelExtract(filePath, source, lang)` currently receives RAW source
(`tree-sitter.ts:6707`); the wasm path applies `extractor.preParse` inside
`TreeSitterExtractor` (`tree-sitter.ts:488`). For kernel-routed c/cpp, apply
the SAME preParse before the kernel call so both arms parse identical
blanked bytes — all seven blanking passes (`preParseCppSource` /
`preParseCSource`, `languages/c-cpp.ts:698/750`) then need NO Rust port,
and every offset survives (they're all equal-length-space replacements).
2. **Metal + CUDA ride the cpp route** (corrected from the first draft):
`.metal`/`.cu` map to language `'cpp'` at detectLanguage (grammars.ts:135),
so there is no separate routing decision — when cpp routes to the kernel,
those files come along as blanked cpp. The preParse hoist MUST pass
`filePath` (the extension gates Metal-attribute blanking) and the CUDA
content gate rides for free. Their suite tests are the parity insurance.
3. **One walker module, dual language** (`codegraph-kernel/src/ccpp/`), flagged
c vs cpp like `tsjs/` flags its four dialects. Grammars: tree-sitter-c +
tree-sitter-cpp crates, wasm vendored from the SAME tags (sha-matched
parser.c + scanner, ts-cli 0.25.10). Upgrade the production wasm FIRST and
get the full suite green before the walker exists (isolate grammar-bump
effects, as R2 did for TS/JS).
## Extractor configs (languages/c-cpp.ts — read the whole file when porting)
**cExtractor (line 180):** functionTypes=[function_definition]; NO
class/method/interface types; structTypes=[struct_specifier];
unionTypes=[union_specifier] (a named `union U { … };` is a definition, and
`typedef union { … } N;` resolves to a first-class `union` node);
enumTypes=[enum_specifier]; enumMemberTypes=[enumerator];
typeAliasTypes=[type_definition]; importTypes=[preproc_include];
callTypes=[call_expression]; variableTypes=[declaration];
nameField=declarator; isConst = any child `type_qualifier` with text
`const`; getReturnType = extractCppReturnType; resolveTypeAliasKind =
typedef enum/struct with body → that kind (anon inner specifier takes the
typedef's name); extractImport: `system_lib_string` → strip `<>`, else
`string_literal>string_content`; signature = full `#include` line.
recoverMangledName = recoverMangledCppName (post-parse salvage, PORT).
**cppExtractor (line 755):** adds classTypes=[class_specifier] with
**skipBodilessClass** (forward decls / elaborated refs mint no node, #1093);
methodTypes=[function_definition] (method vs function decided by
isInsideClassLikeNode); typeAliasTypes += alias_declaration (`using X = …`);
resolveName = extractCppQualifiedMethodName (line 75: macro-recovered name
first, else LAST `::` segment of the declarator's qualified_identifier —
found by BFS that SKIPS parameter_list + trailing_return_type, line 13);
getReceiverType = extractCppReceiverType (line 86: the qualifier prefix,
`stripCppTemplateArgs`-normalized — multi-line template args must not leak
newlines into qualifiedName, #1286/NAME_MAX); getReturnType =
extractCppReturnType → normalizeCppReturnType (line 122: smart-ptr/optional
unwrap to pointee, cv/template/ptr strip, last `::` segment, primitives →
none — set CPP_NON_CLASS_RETURN line 108); getVisibility = nearest preceding
`access_specifier` scan in the parent's children (line 785);
isMisparsedFunction (line 811): name starts `namespace`, name ∈
{switch,if,for,while,do,case,return}, or isMacroMisparsedTypeDecl (line 261:
bodyless class/struct specifier in `type` + non-function_declarator
declarator → DROP the node).
**Name-salvage helpers to port exactly:** recoverCppMacroDefinedName (line
49 — ALL-CAPS-with-underscore parsed name + first param a LONE lowercase
type_identifier + ≥2 params + NO other lone-identifier param; gtest
`TEST_F(Fixture, Name)` / `PYBIND11_MODULE(ext, m)` bail);
recoverMangledCppName (line 406 — only already-mangled names, `Ret (name)`
idiom left alone, last token before `(`, primitive/keyword guard);
stripCppTemplateArgs (line 157 — depth-counted removal of every balanced
`<…>`); cDeclaratorIdentifier (tree-sitter.ts:234 — declarator chain walk,
function_declarator → null, 12-hop guard).
## tree-sitter.ts branches (anchors as of `705e501`)
| Line | Mechanism | Must-mirror details |
|---|---|---|
| 962 | cpp namespace prefix stack (#1291) | named `namespace_definition` pushes its name (C++17 `a::b` as written) onto the QN prefix while walking children; anonymous falls through bare |
| 2795 | C file-scope variables | only when NO function ancestor; iterate declarators; accept ONLY init_declarator / pointer_declarator / array_declarator — a BARE identifier declarator is a macro-prototype misparse, skip (loses uninit scalars by design); name via cDeclaratorIdentifier; signature `= <first 100 chars>`; kind constant/variable via isConst |
| 4313 | explicit operator calls (#1247) | callee = `function` field + ERROR-wrapped `operator_name` sibling; compact symbolic spacing (`operator *``operator*`, word forms keep space); receiver `->``.`; DROP unless receiver is `this` (bare name) or simple identifier/member chain (silent miss over wrong edge) |
| ~4340 | field_expression method calls | `recv.method`/`ptr->method``recv.method`; SKIP_RECEIVERS {self,this,cls,super} → bare name; LITERAL receiver → emit nothing (#1230) |
| 4398 | call-result receivers (#645/#608) | receiver is call_expression → `<innerCallee>().<method>` re-encode (c AND cpp in the gate list) |
| 4534 | template-arg strip on callees | callee contains `<` and NOT `operator` → stripCppTemplateArgs (CUDA launch sites post-blank take this shape) |
| 4545 | local fn-ptr call fan-out (#932-adjacent) | bare-identifier callee found in cppLocalFnPtrs[callerId] → emit one `calls` ref PER recorded target, suppress the local name |
| 5173 | stack construction (#1035) | cpp `declaration` where `type` ∈ {type_identifier, template_type, qualified_identifier} and any init_declarator has `value` ∈ {argument_list, initializer_list} → extractInstantiation |
| 5183 | fn-ptr binding recording | inside a body: `declaration>init_declarator` (identifier declarator) or `assignment_expression` (identifier left); value must be pointer_expression whose child(0) is `&`; target ∈ {identifier, template_function, qualified_identifier}, template-stripped; per-callerId map of per-local Sets (branch reassignments accumulate) |
| 5408 | base_class_clause → extends (#1043) | per base: type_identifier / qualified_identifier / template_type, stripCppTemplateArgs'd; access-specifier keywords skipped |
| 4740 | static member refs — **cpp only** (c not in STATIC_MEMBER_LANGS, line 345) | `Foo::BAR` value reads → `references` edge; VERIFY the MEMBER_ACCESS_TYPES shapes for qualified_identifier + the call-callee skip during the port |
| — | value-reference edges | **c: YES** (VALUE_REF_LANGS line 401), **cpp: NO**. Port the value-ref machinery for C only (crib go.rs / tsjs — shadow prune, scope stack, MAX_VALUE_REF_NODES cap, CODEGRAPH_VALUE_REFS=0 kill) |
| — | fn-ref capture (#756) | function-ref.ts:376: `c: cFamilySpec()`, `cpp: cFamilySpec({ addressOfOnly: true })`; note line ~582: `&Cls::m` exemption from the bare-ids-are-free-functions rule — read cFamilySpec fully when porting |
| 355 | INSTANTIATION_KINDS | includes new_expression → cpp `new Foo(...)` instantiates (verify the cpp entry list when porting) |
**Generic paths c/cpp share with ported languages** (already mirrored by
java.rs/python.rs/go.rs — re-verify each against c/cpp fixtures rather than
re-deriving): extractFunction/Method QN via scope stack + receiverType
(`Recv::name` like Go), struct/enum/enum_member extraction, typedef →
type_alias (+ resolveTypeAliasKind kind override), imports, docstrings
(preceding `comment` nodes — docstring.rs already handles C markers incl. the
CRLF semantics from #1329), contains edges, signature truncation in UTF-16
units, MAX_FILE_SIZE / generated-file skips, `has_error()``defer:`.
## Gates (per plan §5, no exceptions)
- Torture fixtures: `torture.c` (fn-ptr tables, typedef enum/struct, file-scope
consts incl. multi-declarator, macro-prototype misparse shape, value-refs) +
`torture.cpp`/`torture.hpp` (namespaces incl. C++17 nested, out-of-line
`Cls::method` defs, templates + template bases, operators incl. spaced call
sites, stack construction, local fn-ptrs, UE-macro shapes THROUGH the
hoisted preParse, `using` aliases, anonymous namespace, access specifiers).
- Parity sweeps + full-init dump-diffs byte-identical: redis + git (C),
fmt + a protobuf-class repo (C++), plus a UE-macro-heavy spot-check
(blanking-hoist parity) — then linux in cg1212 (expect parse 338s →
~120180s at the 2c envelope; graph counts must stay 2,048,664/6,405,964).
- Deferral-rate guard <10%; suite; changelog rides the existing kernel entry.
- DEFAULT_ROUTED += c, cpp only after ALL of the above.