lib.rs 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  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. mod buffers;
  18. mod ccpp;
  19. mod cfnptr;
  20. mod csharp;
  21. mod dart;
  22. mod docstring;
  23. mod ids;
  24. mod go;
  25. mod java;
  26. mod kotlin;
  27. mod langs;
  28. mod lua;
  29. mod php;
  30. mod rlang;
  31. mod ruby;
  32. mod rustlang;
  33. mod scala;
  34. mod swift;
  35. mod textutil;
  36. mod python;
  37. mod tsjs;
  38. use napi::bindgen_prelude::*;
  39. use napi_derive::napi;
  40. /// The five flat tables for one file. See buffers.rs for the byte layout;
  41. /// `src/extraction/kernel/layout.ts` is the TS mirror.
  42. #[napi(object)]
  43. pub struct ExtractBuffers {
  44. pub meta: Buffer,
  45. pub nodes: Buffer,
  46. pub edges: Buffer,
  47. pub refs: Buffer,
  48. pub arena: Buffer,
  49. }
  50. /// Wire-contract description — the TS loader verifies this against
  51. /// src/types.ts before routing anything to the kernel, so an out-of-date
  52. /// `.node` degrades to the wasm path instead of mis-decoding.
  53. #[napi(object)]
  54. pub struct ContractInfo {
  55. pub abi_version: u32,
  56. pub kernel_version: String,
  57. pub node_kinds: Vec<String>,
  58. pub edge_kinds: Vec<String>,
  59. /// Languages this binary can extract (routing is still TS-side policy).
  60. pub languages: Vec<String>,
  61. }
  62. /// Grammar identity for the grammar-source-parity gate: the wasm grammar and
  63. /// the native grammar must expose identical node-kind/field tables, or
  64. /// kernel-vs-fallback routing would be non-deterministic.
  65. #[napi(object)]
  66. pub struct GrammarInfo {
  67. pub abi_version: u32,
  68. pub node_kind_count: u32,
  69. pub field_count: u32,
  70. pub node_kinds: Vec<String>,
  71. pub field_names: Vec<String>,
  72. }
  73. #[napi]
  74. pub fn contract_info() -> ContractInfo {
  75. ContractInfo {
  76. abi_version: buffers::KERNEL_ABI_VERSION as u32,
  77. kernel_version: env!("CARGO_PKG_VERSION").to_string(),
  78. node_kinds: buffers::NODE_KINDS.iter().map(|s| s.to_string()).collect(),
  79. edge_kinds: buffers::EDGE_KINDS.iter().map(|s| s.to_string()).collect(),
  80. languages: langs::LANGUAGES.iter().map(|s| s.to_string()).collect(),
  81. }
  82. }
  83. #[napi]
  84. pub fn grammar_info(language: String) -> Option<GrammarInfo> {
  85. let lang = langs::grammar_for(&language)?;
  86. let node_kind_count = lang.node_kind_count();
  87. let field_count = lang.field_count();
  88. let node_kinds = (0..node_kind_count)
  89. .map(|i| lang.node_kind_for_id(i as u16).unwrap_or("").to_string())
  90. .collect();
  91. // Field ids are 1-based in tree-sitter.
  92. let field_names = (1..=field_count)
  93. .map(|i| lang.field_name_for_id(i as u16).unwrap_or("").to_string())
  94. .collect();
  95. Some(GrammarInfo {
  96. abi_version: lang.abi_version() as u32,
  97. node_kind_count: node_kind_count as u32,
  98. field_count: field_count as u32,
  99. node_kinds,
  100. field_names,
  101. })
  102. }
  103. /// One struct node's extent for the cFnPtr sweep (mirror of the TS caller's
  104. /// `{ id, startLine, endLine }`, with `endLine ?? startLine` applied TS-side).
  105. #[napi(object)]
  106. pub struct CfnptrStructIn {
  107. pub id: String,
  108. pub start_line: u32,
  109. pub end_line: u32,
  110. }
  111. #[napi(object)]
  112. pub struct CfnptrFileIn {
  113. /// RAW file text, exactly as the resolver's readFile returned it.
  114. pub text: String,
  115. pub structs: Vec<CfnptrStructIn>,
  116. }
  117. #[napi(object)]
  118. pub struct CfnptrField {
  119. pub name: String,
  120. pub index: u32,
  121. pub ptr: bool,
  122. #[napi(js_name = "type")]
  123. pub ty: String,
  124. }
  125. #[napi(object)]
  126. pub struct CfnptrStructOut {
  127. pub id: String,
  128. pub parsed: bool,
  129. pub fields: Vec<CfnptrField>,
  130. }
  131. /// The cFnPtr extraction-sweep facts for one file — see cfnptr.rs (and the
  132. /// TS synthesizer's `FileFacts`) for field semantics.
  133. #[napi(object)]
  134. pub struct CfnptrFacts {
  135. pub fn_ptr_typedefs: Vec<String>,
  136. pub fn_type_typedefs: Vec<String>,
  137. pub structs: Vec<CfnptrStructOut>,
  138. pub inline_ptr: bool,
  139. pub inline_types: Vec<String>,
  140. pub inline_tags: Vec<String>,
  141. pub init_tokens: Vec<String>,
  142. pub array_elems: Vec<String>,
  143. pub alias_names: Vec<String>,
  144. pub d_pairs: Vec<String>,
  145. pub dispatch_fields: Vec<String>,
  146. pub array_dispatch_names: Vec<String>,
  147. pub includes: Vec<String>,
  148. }
  149. /// Batched cFnPtr extraction sweep (task #5 step 2): one call scans a batch
  150. /// of files and returns their collected facts, amortizing the NAPI boundary.
  151. /// Feature-detected by the TS loader — absent on older binaries, where the
  152. /// synthesizer keeps its JS sweep.
  153. #[napi]
  154. pub fn cfnptr_scan_files(files: Vec<CfnptrFileIn>) -> Vec<CfnptrFacts> {
  155. files
  156. .into_iter()
  157. .map(|f| {
  158. let structs: Vec<cfnptr::StructExtent> = f
  159. .structs
  160. .into_iter()
  161. .map(|s| cfnptr::StructExtent { id: s.id, start_line: s.start_line, end_line: s.end_line })
  162. .collect();
  163. let facts = cfnptr::scan_file(&f.text, &structs);
  164. CfnptrFacts {
  165. fn_ptr_typedefs: facts.fn_ptr_typedefs,
  166. fn_type_typedefs: facts.fn_type_typedefs,
  167. structs: facts
  168. .structs
  169. .into_iter()
  170. .map(|s| CfnptrStructOut {
  171. id: s.id,
  172. parsed: s.parsed,
  173. fields: s
  174. .fields
  175. .into_iter()
  176. .map(|fl| CfnptrField { name: fl.name, index: fl.index, ptr: fl.ptr, ty: fl.ty })
  177. .collect(),
  178. })
  179. .collect(),
  180. inline_ptr: facts.inline_ptr,
  181. inline_types: facts.inline_types,
  182. inline_tags: facts.inline_tags,
  183. init_tokens: facts.init_tokens,
  184. array_elems: facts.array_elems,
  185. alias_names: facts.alias_names,
  186. d_pairs: facts.d_pairs,
  187. dispatch_fields: facts.dispatch_fields,
  188. array_dispatch_names: facts.array_dispatch_names,
  189. includes: facts.includes,
  190. }
  191. })
  192. .collect()
  193. }
  194. /// Debug/differential hook: the native `stripCommentsForRegex(text, 'c')`.
  195. /// Exists so the strip differential oracle can pin the Rust stripper against
  196. /// the TS reference directly.
  197. #[napi]
  198. pub fn cfnptr_strip_c(text: String) -> String {
  199. String::from_utf8_lossy(&cfnptr::strip_c(text.as_bytes())).into_owned()
  200. }
  201. #[napi]
  202. pub fn extract_file(file_path: String, content: String, language: String) -> Result<ExtractBuffers> {
  203. let out = match language.as_str() {
  204. "java" => java::extract(&file_path, &content).map_err(Error::from_reason)?,
  205. "python" => python::extract(&file_path, &content).map_err(Error::from_reason)?,
  206. "go" => go::extract(&file_path, &content).map_err(Error::from_reason)?,
  207. "c" | "cpp" => ccpp::extract(&file_path, &content, &language).map_err(Error::from_reason)?,
  208. "rust" => rustlang::extract(&file_path, &content).map_err(Error::from_reason)?,
  209. "csharp" => csharp::extract(&file_path, &content).map_err(Error::from_reason)?,
  210. "ruby" => ruby::extract(&file_path, &content).map_err(Error::from_reason)?,
  211. "php" => php::extract(&file_path, &content).map_err(Error::from_reason)?,
  212. "swift" => swift::extract(&file_path, &content).map_err(Error::from_reason)?,
  213. "kotlin" => kotlin::extract(&file_path, &content).map_err(Error::from_reason)?,
  214. "r" => rlang::extract(&file_path, &content).map_err(Error::from_reason)?,
  215. "lua" | "luau" => lua::extract(&file_path, &content, &language).map_err(Error::from_reason)?,
  216. "scala" => scala::extract(&file_path, &content).map_err(Error::from_reason)?,
  217. "dart" => dart::extract(&file_path, &content).map_err(Error::from_reason)?,
  218. _ => tsjs::extract(&file_path, &content, &language).map_err(Error::from_reason)?,
  219. };
  220. Ok(ExtractBuffers {
  221. meta: out.meta.into(),
  222. nodes: out.nodes.into(),
  223. edges: out.edges.into(),
  224. refs: out.refs.into(),
  225. arena: out.arena.into(),
  226. })
  227. }