lib.rs 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. //! codegraph-kernel — native extraction kernel (napi-rs).
  2. //!
  3. //! Replaces ONLY the parse+extract walk inside the parse workers, behind the
  4. //! existing `ExtractionResult` contract. Input `(filePath, content, language)`
  5. //! per file; output flat typed buffers — one boundary crossing per file.
  6. //! Everything downstream (resolution, synthesis, frameworks, MCP) is
  7. //! untouched and consumes the decoded result exactly as before.
  8. //!
  9. //! Calls are synchronous by design: the existing `ParseWorkerPool` workers
  10. //! already parallelize per-file, so each worker thread drives its own kernel
  11. //! call (do NOT rebuild the pool on the Rust side — see the migration plan §3).
  12. //!
  13. //! Per-language extraction lives in a dedicated walker module (tsjs/ for
  14. //! typescript/tsx/javascript/jsx) that mirrors the TS extractor for behavioral
  15. //! parity — verified by scripts/kernel-parity.mjs and the §5 gate.
  16. #![deny(clippy::all)]
  17. /// First statement of every recursive walker function (see stack.rs, #1581):
  18. /// once the stack pointer is inside the red zone, stop descending — the
  19. /// latched flag makes `stack::run_guarded` discard the walk and defer the
  20. /// file to wasm. `Default::default()` covers every walker return type in use
  21. /// (`()`, `bool`, `Option<_>`, `String`); a hook returning `false` just sends
  22. /// its caller down the generic child walk, whose own guard returns at once.
  23. macro_rules! stack_guard {
  24. () => {
  25. if $crate::stack::exhausted() {
  26. return ::core::default::Default::default();
  27. }
  28. };
  29. }
  30. mod buffers;
  31. mod ccpp;
  32. mod cfnptr;
  33. mod csharp;
  34. mod dart;
  35. mod docstring;
  36. mod ids;
  37. mod go;
  38. mod java;
  39. mod kotlin;
  40. mod langs;
  41. mod lua;
  42. mod php;
  43. mod rlang;
  44. mod ruby;
  45. mod rustlang;
  46. mod scala;
  47. mod stack;
  48. mod swift;
  49. mod textutil;
  50. mod python;
  51. mod tsjs;
  52. use napi::bindgen_prelude::*;
  53. use napi_derive::napi;
  54. /// The five flat tables for one file. See buffers.rs for the byte layout;
  55. /// `src/extraction/kernel/layout.ts` is the TS mirror.
  56. #[napi(object)]
  57. pub struct ExtractBuffers {
  58. pub meta: Buffer,
  59. pub nodes: Buffer,
  60. pub edges: Buffer,
  61. pub refs: Buffer,
  62. pub arena: Buffer,
  63. }
  64. /// Wire-contract description — the TS loader verifies this against
  65. /// src/types.ts before routing anything to the kernel, so an out-of-date
  66. /// `.node` degrades to the wasm path instead of mis-decoding.
  67. #[napi(object)]
  68. pub struct ContractInfo {
  69. pub abi_version: u32,
  70. pub kernel_version: String,
  71. pub node_kinds: Vec<String>,
  72. pub edge_kinds: Vec<String>,
  73. /// Languages this binary can extract (routing is still TS-side policy).
  74. pub languages: Vec<String>,
  75. }
  76. /// Grammar identity for the grammar-source-parity gate: the wasm grammar and
  77. /// the native grammar must expose identical node-kind/field tables, or
  78. /// kernel-vs-fallback routing would be non-deterministic.
  79. #[napi(object)]
  80. pub struct GrammarInfo {
  81. pub abi_version: u32,
  82. pub node_kind_count: u32,
  83. pub field_count: u32,
  84. pub node_kinds: Vec<String>,
  85. pub field_names: Vec<String>,
  86. }
  87. #[napi]
  88. pub fn contract_info() -> ContractInfo {
  89. ContractInfo {
  90. abi_version: buffers::KERNEL_ABI_VERSION as u32,
  91. kernel_version: env!("CARGO_PKG_VERSION").to_string(),
  92. node_kinds: buffers::NODE_KINDS.iter().map(|s| s.to_string()).collect(),
  93. edge_kinds: buffers::EDGE_KINDS.iter().map(|s| s.to_string()).collect(),
  94. languages: langs::LANGUAGES.iter().map(|s| s.to_string()).collect(),
  95. }
  96. }
  97. #[napi]
  98. pub fn grammar_info(language: String) -> Option<GrammarInfo> {
  99. let lang = langs::grammar_for(&language)?;
  100. let node_kind_count = lang.node_kind_count();
  101. let field_count = lang.field_count();
  102. let node_kinds = (0..node_kind_count)
  103. .map(|i| lang.node_kind_for_id(i as u16).unwrap_or("").to_string())
  104. .collect();
  105. // Field ids are 1-based in tree-sitter.
  106. let field_names = (1..=field_count)
  107. .map(|i| lang.field_name_for_id(i as u16).unwrap_or("").to_string())
  108. .collect();
  109. Some(GrammarInfo {
  110. abi_version: lang.abi_version() as u32,
  111. node_kind_count: node_kind_count as u32,
  112. field_count: field_count as u32,
  113. node_kinds,
  114. field_names,
  115. })
  116. }
  117. /// One struct node's extent for the cFnPtr sweep (mirror of the TS caller's
  118. /// `{ id, startLine, endLine }`, with `endLine ?? startLine` applied TS-side).
  119. #[napi(object)]
  120. pub struct CfnptrStructIn {
  121. pub id: String,
  122. pub start_line: u32,
  123. pub end_line: u32,
  124. }
  125. #[napi(object)]
  126. pub struct CfnptrFileIn {
  127. /// RAW file text, exactly as the resolver's readFile returned it.
  128. pub text: String,
  129. pub structs: Vec<CfnptrStructIn>,
  130. }
  131. #[napi(object)]
  132. pub struct CfnptrField {
  133. pub name: String,
  134. pub index: u32,
  135. pub ptr: bool,
  136. #[napi(js_name = "type")]
  137. pub ty: String,
  138. }
  139. #[napi(object)]
  140. pub struct CfnptrStructOut {
  141. pub id: String,
  142. pub parsed: bool,
  143. pub fields: Vec<CfnptrField>,
  144. }
  145. /// The cFnPtr extraction-sweep facts for one file — see cfnptr.rs (and the
  146. /// TS synthesizer's `FileFacts`) for field semantics.
  147. #[napi(object)]
  148. pub struct CfnptrFacts {
  149. pub fn_ptr_typedefs: Vec<String>,
  150. pub fn_type_typedefs: Vec<String>,
  151. pub structs: Vec<CfnptrStructOut>,
  152. pub inline_ptr: bool,
  153. pub inline_types: Vec<String>,
  154. pub inline_tags: Vec<String>,
  155. pub init_tokens: Vec<String>,
  156. pub array_elems: Vec<String>,
  157. pub alias_names: Vec<String>,
  158. pub d_pairs: Vec<String>,
  159. pub dispatch_fields: Vec<String>,
  160. pub array_dispatch_names: Vec<String>,
  161. pub includes: Vec<String>,
  162. }
  163. /// Batched cFnPtr extraction sweep (task #5 step 2): one call scans a batch
  164. /// of files and returns their collected facts, amortizing the NAPI boundary.
  165. /// Feature-detected by the TS loader — absent on older binaries, where the
  166. /// synthesizer keeps its JS sweep.
  167. #[napi]
  168. pub fn cfnptr_scan_files(files: Vec<CfnptrFileIn>) -> Vec<CfnptrFacts> {
  169. files
  170. .into_iter()
  171. .map(|f| {
  172. let structs: Vec<cfnptr::StructExtent> = f
  173. .structs
  174. .into_iter()
  175. .map(|s| cfnptr::StructExtent { id: s.id, start_line: s.start_line, end_line: s.end_line })
  176. .collect();
  177. let facts = cfnptr::scan_file(&f.text, &structs);
  178. CfnptrFacts {
  179. fn_ptr_typedefs: facts.fn_ptr_typedefs,
  180. fn_type_typedefs: facts.fn_type_typedefs,
  181. structs: facts
  182. .structs
  183. .into_iter()
  184. .map(|s| CfnptrStructOut {
  185. id: s.id,
  186. parsed: s.parsed,
  187. fields: s
  188. .fields
  189. .into_iter()
  190. .map(|fl| CfnptrField { name: fl.name, index: fl.index, ptr: fl.ptr, ty: fl.ty })
  191. .collect(),
  192. })
  193. .collect(),
  194. inline_ptr: facts.inline_ptr,
  195. inline_types: facts.inline_types,
  196. inline_tags: facts.inline_tags,
  197. init_tokens: facts.init_tokens,
  198. array_elems: facts.array_elems,
  199. alias_names: facts.alias_names,
  200. d_pairs: facts.d_pairs,
  201. dispatch_fields: facts.dispatch_fields,
  202. array_dispatch_names: facts.array_dispatch_names,
  203. includes: facts.includes,
  204. }
  205. })
  206. .collect()
  207. }
  208. /// Debug/differential hook: the native `stripCommentsForRegex(text, 'c')`.
  209. /// Exists so the strip differential oracle can pin the Rust stripper against
  210. /// the TS reference directly.
  211. #[napi]
  212. pub fn cfnptr_strip_c(text: String) -> String {
  213. String::from_utf8_lossy(&cfnptr::strip_c(text.as_bytes())).into_owned()
  214. }
  215. #[napi]
  216. pub fn extract_file(file_path: String, content: String, language: String) -> Result<ExtractBuffers> {
  217. // The whole walk runs under the stack guard (stack.rs, #1581): a file
  218. // nested deeply enough to overflow this thread's stack comes back as a
  219. // `defer:` error — the TS side's routine "take the wasm path" signal —
  220. // instead of a SIGSEGV that kills the entire indexer process.
  221. let out = stack::run_guarded(|| match language.as_str() {
  222. "java" => java::extract(&file_path, &content),
  223. "python" => python::extract(&file_path, &content),
  224. "go" => go::extract(&file_path, &content),
  225. "c" | "cpp" => ccpp::extract(&file_path, &content, &language),
  226. "rust" => rustlang::extract(&file_path, &content),
  227. "csharp" => csharp::extract(&file_path, &content),
  228. "ruby" => ruby::extract(&file_path, &content),
  229. "php" => php::extract(&file_path, &content),
  230. "swift" => swift::extract(&file_path, &content),
  231. "kotlin" => kotlin::extract(&file_path, &content),
  232. "r" => rlang::extract(&file_path, &content),
  233. "lua" | "luau" => lua::extract(&file_path, &content, &language),
  234. "scala" => scala::extract(&file_path, &content),
  235. "dart" => dart::extract(&file_path, &content),
  236. _ => tsjs::extract(&file_path, &content, &language),
  237. })
  238. .map_err(Error::from_reason)?;
  239. Ok(ExtractBuffers {
  240. meta: out.meta.into(),
  241. nodes: out.nodes.into(),
  242. edges: out.edges.into(),
  243. refs: out.refs.into(),
  244. arena: out.arena.into(),
  245. })
  246. }