Files
codegraph/src/extraction/kernel/loader.ts
T
Colby Mchenry 69ea438bac perf(kernel): cFnPtr native extraction sweep — step 2, pass 230→151s across the arc (§7a.10) (#1365)
Task #5 step 2. The fuse-then-link refactor (#1364) left the extraction
sweep as a clean per-file boundary: raw text in → collected facts out.
This ports that sweep to the native kernel: `cfnptr_scan_files`
(codegraph-kernel/src/cfnptr.rs) strips and scans a batch of 16 files
per NAPI call, and the TS side only reads files, ships batches, interns
the returned facts, and resolves include paths. The JS sweep remains as
the fallback (no binary, feature detection against older binaries,
CODEGRAPH_KERNEL=0, or CODEGRAPH_KERNEL_CFNPTR=0).

Parity discipline: the JS regexes are the spec, so the scanners are
hand-rolled byte machines reproducing that engine — ASCII \w/\b next to
UNICODE \s (NBSP/U+2000-200A/FEFF decoded from UTF-8), alternation
order, lastIndex resume, and the observable backtracking dimensions
(INIT/ARRAY modifier and struct/star/bracket optionals, DISPATCH's
greedy segment loop); greedy shortcuts only where backtracking provably
can't rescue a match. The native stripper blanks per UTF-16 code unit,
so its output is string-identical to the TS stripper — pinned by a new
kernel arm on the strip differential oracle (fixtures + 500 seeded
random cases).

Gates, all green: new differential suite (adversarial fixture project —
CRLF, NBSP, continuations, decoy strings, unterminated comments,
backtracking shapes — indexed native-vs-JS: identical edge streams,
plus a record-level scanner check); repo differential on
git/redis/vim/SameBoy (identical, 705/852/433/180 edges); probe-hash on
the live linux kernel DB reproduced f6e1713d… (279,335 rows); linux
init counts exact 2,049,153/6,413,518; dump sha 6dd1185b… reproduced
(10,446,478 lines); full suite green ×2 (153 files / 2588 tests).

Measured (8c cg1212, quiet host): cFnPtr sub A=47.9s B=1.1 C=40.9
D=24.1 E=36.8 = 150.9s vs step 1's 179s and the pre-arc 230s (−34%
cumulative); the sweep itself halved (94.5→47.9s, JS strips
132.4k→68.9k). callback-synthesis phase 199.9→171.1s. E's attributed
wall grew from overlap shift under parallel synthesis; the phase total
is the honest number. Full record: plan §7a.10.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 17:48:27 -05:00

180 lines
6.6 KiB
TypeScript

/**
* Native-kernel loader — finds, loads, and contract-verifies the
* codegraph-kernel .node addon.
*
* The kernel is OPTIONAL everywhere. Every failure mode here (no binary for
* this platform, dlopen error, ABI/kind-table mismatch) resolves to `null`
* and the extraction path silently keeps using the wasm pipeline — a missing
* or stale kernel must never break indexing, only skip the speedup. Set
* CODEGRAPH_KERNEL_DEBUG=1 to see why a kernel didn't load.
*
* Kill switch: CODEGRAPH_KERNEL=0 disables the kernel entirely (checked per
* call so tests and embedders can flip it at runtime).
*
* Search order:
* 1. CODEGRAPH_KERNEL_PATH — explicit .node path (dev/testing override)
* 2. <up3>/kernel/codegraph-kernel.node — the release bundle layout
* (lib/dist/** next to lib/kernel/; see scripts/build-bundle.sh)
* 3. <up3>/codegraph-kernel/prebuilds/<platform>-<arch>/codegraph-kernel.node
* — from-source runs and tests (staged by scripts/build-kernel.sh)
*
* "up3" = three directories above this file, which is the package root both
* from src/extraction/kernel/ and from dist/extraction/kernel/.
*/
import * as fs from 'fs';
import * as path from 'path';
import { createRequire } from 'module';
import { NODE_KINDS, EDGE_KINDS } from '../../types';
import { KERNEL_ABI_VERSION } from './layout';
/** Raw buffer tables for one file — see layout.ts for the byte layout. */
export interface KernelBuffers {
meta: Buffer;
nodes: Buffer;
edges: Buffer;
refs: Buffer;
arena: Buffer;
}
export interface KernelContractInfo {
abiVersion: number;
kernelVersion: string;
nodeKinds: string[];
edgeKinds: string[];
languages: string[];
}
export interface KernelGrammarInfo {
abiVersion: number;
nodeKindCount: number;
fieldCount: number;
nodeKinds: string[];
fieldNames: string[];
}
/** Input to the cFnPtr extraction sweep: one file's raw text + its struct
* node extents (`endLine ?? startLine` applied by the caller). */
export interface CfnptrFileIn {
text: string;
structs: { id: string; startLine: number; endLine: number }[];
}
/** Per-file facts from the native cFnPtr extraction sweep — mirror of the
* Rust `CfnptrFacts` (see codegraph-kernel/src/cfnptr.rs); semantics match
* the JS sweep in src/resolution/c-fnptr-synthesizer.ts. */
export interface CfnptrFactsOut {
fnPtrTypedefs: string[];
fnTypeTypedefs: string[];
structs: { id: string; parsed: boolean; fields: { name: string; index: number; ptr: boolean; type: string }[] }[];
inlinePtr: boolean;
inlineTypes: string[];
inlineTags: string[];
initTokens: string[];
arrayElems: string[];
aliasNames: string[];
dPairs: string[];
dispatchFields: string[];
arrayDispatchNames: string[];
includes: string[];
}
export interface KernelModule {
extractFile(filePath: string, content: string, language: string): KernelBuffers;
contractInfo(): KernelContractInfo;
grammarInfo(language: string): KernelGrammarInfo | null;
/** Batched cFnPtr extraction sweep (task #5 step 2). OPTIONAL: absent on
* older binaries — callers feature-detect and keep their JS path. */
cfnptrScanFiles?(files: CfnptrFileIn[]): CfnptrFactsOut[];
/** Native `stripCommentsForRegex(text, 'c')` — differential-oracle hook. */
cfnptrStripC?(text: string): string;
}
const debugEnabled = () => process.env.CODEGRAPH_KERNEL_DEBUG === '1';
function debug(msg: string): void {
if (debugEnabled()) process.stderr.write(`[codegraph-kernel] ${msg}\n`);
}
/** Languages the loaded binary supports (contract-verified). Empty when no kernel. */
let kernelLanguages: ReadonlySet<string> = new Set();
/** undefined = not attempted yet; null = attempted and unavailable. */
let cached: KernelModule | null | undefined;
function candidatePaths(): string[] {
const candidates: string[] = [];
if (process.env.CODEGRAPH_KERNEL_PATH) candidates.push(process.env.CODEGRAPH_KERNEL_PATH);
const packageRoot = path.resolve(__dirname, '..', '..', '..');
candidates.push(path.join(packageRoot, 'kernel', 'codegraph-kernel.node'));
candidates.push(
path.join(
packageRoot,
'codegraph-kernel',
'prebuilds',
`${process.platform}-${process.arch}`,
'codegraph-kernel.node'
)
);
return candidates;
}
/**
* Verify the binary speaks our wire contract: same ABI version and byte-equal
* NodeKind/EdgeKind tables (kinds cross the boundary as indexes into these).
*/
function verifyContract(mod: KernelModule, from: string): boolean {
const info = mod.contractInfo();
if (info.abiVersion !== KERNEL_ABI_VERSION) {
debug(`${from}: ABI ${info.abiVersion} != expected ${KERNEL_ABI_VERSION} — ignoring kernel`);
return false;
}
const sameTable = (a: readonly string[], b: readonly string[]) =>
a.length === b.length && a.every((v, i) => v === b[i]);
if (!sameTable(info.nodeKinds, NODE_KINDS) || !sameTable(info.edgeKinds, EDGE_KINDS)) {
debug(`${from}: NodeKind/EdgeKind tables differ from src/types.ts — ignoring kernel`);
return false;
}
return true;
}
/**
* Load (once per process) and return the kernel module, or null when
* unavailable. The kill switch is NOT checked here — callers route through
* `kernelAvailable()` / `tryKernelExtract()` which check it per call.
*/
export function getKernel(): KernelModule | null {
if (cached !== undefined) return cached;
cached = null;
for (const candidate of candidatePaths()) {
try {
if (!fs.existsSync(candidate)) continue;
// createRequire: works identically from CJS output and future ESM.
const req = createRequire(__filename);
const mod = req(candidate) as KernelModule;
if (typeof mod.extractFile !== 'function' || typeof mod.contractInfo !== 'function') {
debug(`${candidate}: missing expected exports — ignoring`);
continue;
}
if (!verifyContract(mod, candidate)) continue;
kernelLanguages = new Set(mod.contractInfo().languages);
debug(`loaded ${candidate} (languages: ${[...kernelLanguages].join(', ')})`);
cached = mod;
break;
} catch (err) {
debug(`${candidate}: failed to load — ${err instanceof Error ? err.message : String(err)}`);
}
}
return cached;
}
/** True when the kill switch is off, a verified binary is loaded, and it supports `language`. */
export function kernelSupports(language: string): boolean {
if (process.env.CODEGRAPH_KERNEL === '0') return false;
return getKernel() !== null && kernelLanguages.has(language);
}
/** Test hook: forget the loaded module so a changed env is re-evaluated. */
export function resetKernelForTests(): void {
cached = undefined;
kernelLanguages = new Set();
}