Files
codegraph/codegraph-kernel/src/lib.rs
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

224 lines
7.7 KiB
Rust

//! codegraph-kernel — native extraction kernel (napi-rs).
//!
//! Replaces ONLY the parse+extract walk inside the parse workers, behind the
//! existing `ExtractionResult` contract. Input `(filePath, content, language)`
//! per file; output flat typed buffers — one boundary crossing per file.
//! Everything downstream (resolution, synthesis, frameworks, MCP) is
//! untouched and consumes the decoded result exactly as before.
//!
//! Calls are synchronous by design: the existing `ParseWorkerPool` workers
//! already parallelize per-file, so each worker thread drives its own kernel
//! call (do NOT rebuild the pool on the Rust side — see the migration plan §3).
//!
//! Per-language extraction lives in a dedicated walker module (tsjs/ for
//! typescript/tsx/javascript/jsx) that mirrors the TS extractor for behavioral
//! parity — verified by scripts/kernel-parity.mjs and the §5 gate.
#![deny(clippy::all)]
mod buffers;
mod ccpp;
mod cfnptr;
mod docstring;
mod ids;
mod go;
mod java;
mod langs;
mod textutil;
mod python;
mod tsjs;
use napi::bindgen_prelude::*;
use napi_derive::napi;
/// The five flat tables for one file. See buffers.rs for the byte layout;
/// `src/extraction/kernel/layout.ts` is the TS mirror.
#[napi(object)]
pub struct ExtractBuffers {
pub meta: Buffer,
pub nodes: Buffer,
pub edges: Buffer,
pub refs: Buffer,
pub arena: Buffer,
}
/// Wire-contract description — the TS loader verifies this against
/// src/types.ts before routing anything to the kernel, so an out-of-date
/// `.node` degrades to the wasm path instead of mis-decoding.
#[napi(object)]
pub struct ContractInfo {
pub abi_version: u32,
pub kernel_version: String,
pub node_kinds: Vec<String>,
pub edge_kinds: Vec<String>,
/// Languages this binary can extract (routing is still TS-side policy).
pub languages: Vec<String>,
}
/// Grammar identity for the grammar-source-parity gate: the wasm grammar and
/// the native grammar must expose identical node-kind/field tables, or
/// kernel-vs-fallback routing would be non-deterministic.
#[napi(object)]
pub struct GrammarInfo {
pub abi_version: u32,
pub node_kind_count: u32,
pub field_count: u32,
pub node_kinds: Vec<String>,
pub field_names: Vec<String>,
}
#[napi]
pub fn contract_info() -> ContractInfo {
ContractInfo {
abi_version: buffers::KERNEL_ABI_VERSION as u32,
kernel_version: env!("CARGO_PKG_VERSION").to_string(),
node_kinds: buffers::NODE_KINDS.iter().map(|s| s.to_string()).collect(),
edge_kinds: buffers::EDGE_KINDS.iter().map(|s| s.to_string()).collect(),
languages: langs::LANGUAGES.iter().map(|s| s.to_string()).collect(),
}
}
#[napi]
pub fn grammar_info(language: String) -> Option<GrammarInfo> {
let lang = langs::grammar_for(&language)?;
let node_kind_count = lang.node_kind_count();
let field_count = lang.field_count();
let node_kinds = (0..node_kind_count)
.map(|i| lang.node_kind_for_id(i as u16).unwrap_or("").to_string())
.collect();
// Field ids are 1-based in tree-sitter.
let field_names = (1..=field_count)
.map(|i| lang.field_name_for_id(i as u16).unwrap_or("").to_string())
.collect();
Some(GrammarInfo {
abi_version: lang.abi_version() as u32,
node_kind_count: node_kind_count as u32,
field_count: field_count as u32,
node_kinds,
field_names,
})
}
/// One struct node's extent for the cFnPtr sweep (mirror of the TS caller's
/// `{ id, startLine, endLine }`, with `endLine ?? startLine` applied TS-side).
#[napi(object)]
pub struct CfnptrStructIn {
pub id: String,
pub start_line: u32,
pub end_line: u32,
}
#[napi(object)]
pub struct CfnptrFileIn {
/// RAW file text, exactly as the resolver's readFile returned it.
pub text: String,
pub structs: Vec<CfnptrStructIn>,
}
#[napi(object)]
pub struct CfnptrField {
pub name: String,
pub index: u32,
pub ptr: bool,
#[napi(js_name = "type")]
pub ty: String,
}
#[napi(object)]
pub struct CfnptrStructOut {
pub id: String,
pub parsed: bool,
pub fields: Vec<CfnptrField>,
}
/// The cFnPtr extraction-sweep facts for one file — see cfnptr.rs (and the
/// TS synthesizer's `FileFacts`) for field semantics.
#[napi(object)]
pub struct CfnptrFacts {
pub fn_ptr_typedefs: Vec<String>,
pub fn_type_typedefs: Vec<String>,
pub structs: Vec<CfnptrStructOut>,
pub inline_ptr: bool,
pub inline_types: Vec<String>,
pub inline_tags: Vec<String>,
pub init_tokens: Vec<String>,
pub array_elems: Vec<String>,
pub alias_names: Vec<String>,
pub d_pairs: Vec<String>,
pub dispatch_fields: Vec<String>,
pub array_dispatch_names: Vec<String>,
pub includes: Vec<String>,
}
/// Batched cFnPtr extraction sweep (task #5 step 2): one call scans a batch
/// of files and returns their collected facts, amortizing the NAPI boundary.
/// Feature-detected by the TS loader — absent on older binaries, where the
/// synthesizer keeps its JS sweep.
#[napi]
pub fn cfnptr_scan_files(files: Vec<CfnptrFileIn>) -> Vec<CfnptrFacts> {
files
.into_iter()
.map(|f| {
let structs: Vec<cfnptr::StructExtent> = f
.structs
.into_iter()
.map(|s| cfnptr::StructExtent { id: s.id, start_line: s.start_line, end_line: s.end_line })
.collect();
let facts = cfnptr::scan_file(&f.text, &structs);
CfnptrFacts {
fn_ptr_typedefs: facts.fn_ptr_typedefs,
fn_type_typedefs: facts.fn_type_typedefs,
structs: facts
.structs
.into_iter()
.map(|s| CfnptrStructOut {
id: s.id,
parsed: s.parsed,
fields: s
.fields
.into_iter()
.map(|fl| CfnptrField { name: fl.name, index: fl.index, ptr: fl.ptr, ty: fl.ty })
.collect(),
})
.collect(),
inline_ptr: facts.inline_ptr,
inline_types: facts.inline_types,
inline_tags: facts.inline_tags,
init_tokens: facts.init_tokens,
array_elems: facts.array_elems,
alias_names: facts.alias_names,
d_pairs: facts.d_pairs,
dispatch_fields: facts.dispatch_fields,
array_dispatch_names: facts.array_dispatch_names,
includes: facts.includes,
}
})
.collect()
}
/// Debug/differential hook: the native `stripCommentsForRegex(text, 'c')`.
/// Exists so the strip differential oracle can pin the Rust stripper against
/// the TS reference directly.
#[napi]
pub fn cfnptr_strip_c(text: String) -> String {
String::from_utf8_lossy(&cfnptr::strip_c(text.as_bytes())).into_owned()
}
#[napi]
pub fn extract_file(file_path: String, content: String, language: String) -> Result<ExtractBuffers> {
let out = match language.as_str() {
"java" => java::extract(&file_path, &content).map_err(Error::from_reason)?,
"python" => python::extract(&file_path, &content).map_err(Error::from_reason)?,
"go" => go::extract(&file_path, &content).map_err(Error::from_reason)?,
"c" | "cpp" => ccpp::extract(&file_path, &content, &language).map_err(Error::from_reason)?,
_ => tsjs::extract(&file_path, &content, &language).map_err(Error::from_reason)?,
};
Ok(ExtractBuffers {
meta: out.meta.into(),
nodes: out.nodes.into(),
edges: out.edges.into(),
refs: out.refs.into(),
arena: out.arena.into(),
})
}