R7b batch 4 #4 — the FINAL R7b language (docs/design/dart-kernel-port-checklist.md is the authoritative quirk list). The fourth vendored-grammar-C language, with a twist: production dart resolved its wasm from tree-sitter-wasms, whose dart dependency is an UNPINNED github:UserNobody14/tree-sitter-dart — a routine dependency update would have silently changed dart's grammar. This PR byte-copies the shipping 0.1.13 artifact into src/extraction/wasm/ (VENDORED_WASM_LANGS += dart) and compiles the same-commit (d4d8f3e337d8) parser.c/scanner.c in the kernel — table identity proven by the kernel-grammar-parity row. crates.io tree-sitter-dart is the nielsenko fork (different lineage) — rejected. The center of gravity is THE SIBLING-BODY DOUBLE-WALK, reproduced bug-for-bug: dart attaches every function/method body as a NEXT SIBLING of its signature, and the TS walkers consume each body TWICE — once via resolveBody (attributed to the function/method) and once via the enclosing generic walk (attributed to the file/class). Duplicate local-function nodes with the SAME id under different parents, duplicated calls/instantiates refs, and file/class-attributed fn-ref twins all emit in the exact observed interleave (a dedicated fixture pins the duplicate-id rows; the bloc kind-census spot-check pins the counts). Also preserved (probe-pinned): the extractBareCall selector matrix (the first callTypes=[] language — cascades completely invisible, `?.` encodes like `.`, the `ConfigT.load()` calls+references double emission with no callee-of-call skip, capitalized-chain `Foo.create().run` re-encode, const-object callee names); the constructor hooks (unnamed ctor skipped, named ctors/factories renamed to the CTOR name with the class as returnType, `@override (T) m()` record-misparse rescued by class-name validation); operator methods minting `method "<anonymous>"`; static_final_declaration constants via the visitNode hook while instance fields mint NOTHING; the prefixed-return-type prefix bug (`other.OtherClass f()` → returnType `other`); enum `with` mixins silent vs `implements` working; anonymous extensions named after the ON type; deferred imports invisible; named-argument callbacks NOT fn-ref-captured (the Flutter `onPressed:` idiom — future accuracy PR, TS-side first); `async*`/`sync*` NOT async; value-refs with the LIVE dart sibling-body pull and the `$X`-vs-`${X}` interpolation asymmetry; dartdoc kept in all three comment forms with the annotation-broken chain. Gates: parity sweeps first-run 0-diff on shelf/bloc/flutter — 5,815 clean files byte-parity, deferrals 10/21/1341 ≈ the survey's 10/21/~1340 (both-arm grammar reality: empty object patterns — the sealed-class idiom — and unnamed `library;` dominate; --max-deferral 0.3); full-init dumps byte-identical ×3 (shelf 7,959 / bloc 40,026 / flutter 1,855,319 dump lines); bloc per-kind node census identical across arms (the double-walk duplicate rows survive the store identically); kernel-dart-parity suite (7 fixtures + in-memory CRLF variants + double-walk duplicate-id pin + generated-file skip pin + two defer pins); full suite 2,688 green ×2 with CODEGRAPH_KERNEL_EXPECT=1. DEFAULT_ROUTED += dart (20 langs — R7b COMPLETE). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
244 lines
8.6 KiB
Rust
244 lines
8.6 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 csharp;
|
|
mod dart;
|
|
mod docstring;
|
|
mod ids;
|
|
mod go;
|
|
mod java;
|
|
mod kotlin;
|
|
mod langs;
|
|
mod lua;
|
|
mod php;
|
|
mod rlang;
|
|
mod ruby;
|
|
mod rustlang;
|
|
mod scala;
|
|
mod swift;
|
|
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)?,
|
|
"rust" => rustlang::extract(&file_path, &content).map_err(Error::from_reason)?,
|
|
"csharp" => csharp::extract(&file_path, &content).map_err(Error::from_reason)?,
|
|
"ruby" => ruby::extract(&file_path, &content).map_err(Error::from_reason)?,
|
|
"php" => php::extract(&file_path, &content).map_err(Error::from_reason)?,
|
|
"swift" => swift::extract(&file_path, &content).map_err(Error::from_reason)?,
|
|
"kotlin" => kotlin::extract(&file_path, &content).map_err(Error::from_reason)?,
|
|
"r" => rlang::extract(&file_path, &content).map_err(Error::from_reason)?,
|
|
"lua" | "luau" => lua::extract(&file_path, &content, &language).map_err(Error::from_reason)?,
|
|
"scala" => scala::extract(&file_path, &content).map_err(Error::from_reason)?,
|
|
"dart" => dart::extract(&file_path, &content).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(),
|
|
})
|
|
}
|