buffers.rs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. //! Flat buffer contract — the ONE boundary crossing per file.
  2. //!
  3. //! The kernel returns five Buffers: meta, nodes, edges, refs, arena. All rows
  4. //! are fixed-width little-endian; every string is an (offset, len) pair into
  5. //! the UTF-8 arena. `OFFSET == NONE (0xFFFF_FFFF)` means "field absent".
  6. //!
  7. //! THIS FILE AND `src/extraction/kernel/layout.ts` MUST MATCH BYTE FOR BYTE.
  8. //! Any layout change bumps `KERNEL_ABI_VERSION` — the TS loader refuses a
  9. //! version it doesn't know and falls back to the wasm path.
  10. //!
  11. //! Layout (v1):
  12. //!
  13. //! meta (36 bytes):
  14. //! 0 u8 KERNEL_ABI_VERSION
  15. //! 1 [3] pad
  16. //! 4 u32 node count
  17. //! 8 u32 edge count
  18. //! 12 u32 ref count
  19. //! 16 u32 arena byte length
  20. //! 20 u32 errors-JSON arena offset (NONE = no errors)
  21. //! 24 u32 errors-JSON byte length
  22. //! 28 f64 kernel-side wall duration (ms) — introspection only; the TS
  23. //! wrapper measures the ExtractionResult.durationMs it reports
  24. //!
  25. //! node row (96 bytes):
  26. //! 0 u8 NodeKind index (NODE_KINDS order)
  27. //! 1 u8 visibility (0 absent, 1 public, 2 private, 3 protected, 4 internal)
  28. //! 2 u16 bool flags — bit pairs (present, value):
  29. //! 0/1 isExported, 2/3 isAsync, 4/5 isStatic, 6/7 isAbstract
  30. //! 4 u32 startLine (1-based)
  31. //! 8 u32 endLine
  32. //! 12 u32 startColumn (0-based)
  33. //! 16 u32 endColumn
  34. //! 20 str name
  35. //! 28 str qualifiedName
  36. //! 36 str id (kernel-computed: "kind:hash32", or "file:<path>" for the file node)
  37. //! 44 str docstring
  38. //! 52 str signature
  39. //! 60 str decorators (NUL-joined list)
  40. //! 68 str typeParameters (NUL-joined list)
  41. //! 76 str returnType
  42. //! 84 str extraJson (escape hatch: JSON of any extra Node props)
  43. //! 92 u32 metrics slot (reserved for Arc 3.2 per-node code metrics; 0)
  44. //!
  45. //! edge row (44 bytes):
  46. //! 0 u32 source node row index (NONE → use sourceIdStr)
  47. //! 4 u32 target node row index (NONE → use targetIdStr)
  48. //! 8 u8 EdgeKind index (EDGE_KINDS order)
  49. //! 9 u8 provenance (0 absent, 1 tree-sitter, 2 scip, 3 heuristic)
  50. //! 10 u16 pad
  51. //! 12 u32 line (NONE absent)
  52. //! 16 u32 column (NONE absent)
  53. //! 20 str metadataJson
  54. //! 28 str sourceIdStr
  55. //! 36 str targetIdStr
  56. //!
  57. //! ref row (40 bytes):
  58. //! 0 u32 fromNode row index (NONE → use fromNodeIdStr)
  59. //! 4 u8 ReferenceKind (EDGE_KINDS index, or 200 = function_ref)
  60. //! 5 u8 flags — bit 0: ref carries the extracting file's path (v2; the
  61. //! ruby/php visitNode hooks set `filePath: ctx.filePath` on their
  62. //! mixin/trait `implements` refs — decode re-attaches the decode
  63. //! call's own filePath, which is byte-identical)
  64. //! 6 [2] pad
  65. //! 8 u32 line (1-based)
  66. //! 12 u32 column (0-based)
  67. //! 16 str referenceName
  68. //! 24 str candidates (NUL-joined list)
  69. //! 32 str fromNodeIdStr
  70. pub const KERNEL_ABI_VERSION: u8 = 2;
  71. pub const NONE: u32 = 0xFFFF_FFFF;
  72. pub const META_SIZE: usize = 36;
  73. pub const NODE_ROW_SIZE: usize = 96;
  74. pub const EDGE_ROW_SIZE: usize = 44;
  75. pub const REF_ROW_SIZE: usize = 40;
  76. /// Mirror of NODE_KINDS in src/types.ts — order is the wire contract.
  77. pub const NODE_KINDS: [&str; 23] = [
  78. "file",
  79. "module",
  80. "class",
  81. "struct",
  82. "interface",
  83. "trait",
  84. "protocol",
  85. "function",
  86. "method",
  87. "property",
  88. "field",
  89. "variable",
  90. "constant",
  91. "enum",
  92. "enum_member",
  93. "type_alias",
  94. "namespace",
  95. "parameter",
  96. "import",
  97. "export",
  98. "route",
  99. "component",
  100. "union",
  101. ];
  102. /// Mirror of EDGE_KINDS in src/types.ts — order is the wire contract.
  103. pub const EDGE_KINDS: [&str; 12] = [
  104. "contains",
  105. "calls",
  106. "imports",
  107. "exports",
  108. "extends",
  109. "implements",
  110. "references",
  111. "type_of",
  112. "returns",
  113. "instantiates",
  114. "overrides",
  115. "decorates",
  116. ];
  117. /// ReferenceKind code for the internal-only `function_ref` (#756).
  118. pub const FUNCTION_REF_CODE: u8 = 200;
  119. /// Ref-row flag bit 0: the ref carries `filePath` = the extracted file.
  120. pub const REF_FLAG_FILE_PATH: u8 = 1;
  121. pub fn node_kind_index(kind: &str) -> Option<u8> {
  122. NODE_KINDS.iter().position(|k| *k == kind).map(|i| i as u8)
  123. }
  124. pub fn edge_kind_index(kind: &str) -> Option<u8> {
  125. EDGE_KINDS.iter().position(|k| *k == kind).map(|i| i as u8)
  126. }
  127. /// (offset, len) arena reference. `NONE_STR` encodes an absent field.
  128. pub type StrRef = (u32, u32);
  129. pub const NONE_STR: StrRef = (NONE, 0);
  130. /// UTF-8 string arena. Strings are appended verbatim; no dedup (per-file
  131. /// buffers are transient and small — intern later if profiling says so).
  132. #[derive(Default)]
  133. pub struct Arena {
  134. buf: Vec<u8>,
  135. }
  136. impl Arena {
  137. pub fn put(&mut self, s: &str) -> StrRef {
  138. let off = self.buf.len() as u32;
  139. self.buf.extend_from_slice(s.as_bytes());
  140. (off, s.len() as u32)
  141. }
  142. /// Not used by the seed emitter yet — R2 (docstring/signature/etc.). Kept
  143. /// so the arena API is complete alongside the layout it feeds.
  144. #[allow(dead_code)]
  145. pub fn put_opt(&mut self, s: Option<&str>) -> StrRef {
  146. match s {
  147. Some(s) => self.put(s),
  148. None => NONE_STR,
  149. }
  150. }
  151. /// NUL-joined list; absent when the list is empty. (R2 surface: decorators,
  152. /// typeParameters, candidates.)
  153. #[allow(dead_code)]
  154. pub fn put_list(&mut self, items: &[String]) -> StrRef {
  155. if items.is_empty() {
  156. return NONE_STR;
  157. }
  158. let joined = items.join("\0");
  159. self.put(&joined)
  160. }
  161. pub fn len(&self) -> u32 {
  162. self.buf.len() as u32
  163. }
  164. pub fn into_vec(self) -> Vec<u8> {
  165. self.buf
  166. }
  167. }
  168. /// Tri-state booleans packed as (present, value) bit pairs.
  169. #[derive(Default, Clone, Copy)]
  170. pub struct BoolFlags(pub u16);
  171. impl BoolFlags {
  172. pub fn set(&mut self, pair: u16, value: bool) {
  173. self.0 |= 1 << (pair * 2);
  174. if value {
  175. self.0 |= 1 << (pair * 2 + 1);
  176. }
  177. }
  178. }
  179. pub const FLAG_IS_EXPORTED: u16 = 0;
  180. #[allow(dead_code)] // R2 surface — part of the v1 wire contract
  181. pub const FLAG_IS_ASYNC: u16 = 1;
  182. #[allow(dead_code)] // R2 surface — part of the v1 wire contract
  183. pub const FLAG_IS_STATIC: u16 = 2;
  184. #[allow(dead_code)] // R2 surface — part of the v1 wire contract
  185. pub const FLAG_IS_ABSTRACT: u16 = 3;
  186. pub struct NodeRow {
  187. pub kind: u8,
  188. pub visibility: u8,
  189. pub flags: BoolFlags,
  190. pub start_line: u32,
  191. pub end_line: u32,
  192. pub start_column: u32,
  193. pub end_column: u32,
  194. pub name: StrRef,
  195. pub qualified_name: StrRef,
  196. pub id: StrRef,
  197. pub docstring: StrRef,
  198. pub signature: StrRef,
  199. pub decorators: StrRef,
  200. pub type_parameters: StrRef,
  201. pub return_type: StrRef,
  202. pub extra_json: StrRef,
  203. }
  204. pub struct EdgeRow {
  205. pub source_idx: u32,
  206. pub target_idx: u32,
  207. pub kind: u8,
  208. pub provenance: u8,
  209. pub line: u32,
  210. pub column: u32,
  211. pub metadata_json: StrRef,
  212. pub source_id_str: StrRef,
  213. pub target_id_str: StrRef,
  214. }
  215. pub struct RefRow {
  216. pub from_idx: u32,
  217. pub kind: u8,
  218. pub line: u32,
  219. pub column: u32,
  220. pub reference_name: StrRef,
  221. pub candidates: StrRef,
  222. pub from_id_str: StrRef,
  223. }
  224. fn push_str_ref(buf: &mut Vec<u8>, r: StrRef) {
  225. buf.extend_from_slice(&r.0.to_le_bytes());
  226. buf.extend_from_slice(&r.1.to_le_bytes());
  227. }
  228. pub struct Tables {
  229. pub nodes: Vec<u8>,
  230. pub edges: Vec<u8>,
  231. pub refs: Vec<u8>,
  232. pub node_count: u32,
  233. pub edge_count: u32,
  234. pub ref_count: u32,
  235. }
  236. impl Default for Tables {
  237. fn default() -> Self {
  238. Tables {
  239. nodes: Vec::with_capacity(NODE_ROW_SIZE * 64),
  240. edges: Vec::with_capacity(EDGE_ROW_SIZE * 64),
  241. refs: Vec::with_capacity(REF_ROW_SIZE * 64),
  242. node_count: 0,
  243. edge_count: 0,
  244. ref_count: 0,
  245. }
  246. }
  247. }
  248. impl Tables {
  249. pub fn push_node(&mut self, r: &NodeRow) -> u32 {
  250. let buf = &mut self.nodes;
  251. buf.push(r.kind);
  252. buf.push(r.visibility);
  253. buf.extend_from_slice(&r.flags.0.to_le_bytes());
  254. buf.extend_from_slice(&r.start_line.to_le_bytes());
  255. buf.extend_from_slice(&r.end_line.to_le_bytes());
  256. buf.extend_from_slice(&r.start_column.to_le_bytes());
  257. buf.extend_from_slice(&r.end_column.to_le_bytes());
  258. push_str_ref(buf, r.name);
  259. push_str_ref(buf, r.qualified_name);
  260. push_str_ref(buf, r.id);
  261. push_str_ref(buf, r.docstring);
  262. push_str_ref(buf, r.signature);
  263. push_str_ref(buf, r.decorators);
  264. push_str_ref(buf, r.type_parameters);
  265. push_str_ref(buf, r.return_type);
  266. push_str_ref(buf, r.extra_json);
  267. buf.extend_from_slice(&0u32.to_le_bytes()); // metrics slot (Arc 3.2)
  268. let idx = self.node_count;
  269. self.node_count += 1;
  270. idx
  271. }
  272. pub fn push_edge(&mut self, r: &EdgeRow) {
  273. let buf = &mut self.edges;
  274. buf.extend_from_slice(&r.source_idx.to_le_bytes());
  275. buf.extend_from_slice(&r.target_idx.to_le_bytes());
  276. buf.push(r.kind);
  277. buf.push(r.provenance);
  278. buf.extend_from_slice(&0u16.to_le_bytes()); // pad
  279. buf.extend_from_slice(&r.line.to_le_bytes());
  280. buf.extend_from_slice(&r.column.to_le_bytes());
  281. push_str_ref(buf, r.metadata_json);
  282. push_str_ref(buf, r.source_id_str);
  283. push_str_ref(buf, r.target_id_str);
  284. self.edge_count += 1;
  285. }
  286. pub fn push_ref(&mut self, r: &RefRow) {
  287. self.push_ref_flagged(r, 0);
  288. }
  289. pub fn push_ref_flagged(&mut self, r: &RefRow, flags: u8) {
  290. let buf = &mut self.refs;
  291. buf.extend_from_slice(&r.from_idx.to_le_bytes());
  292. buf.push(r.kind);
  293. buf.push(flags);
  294. buf.extend_from_slice(&[0u8; 2]); // pad
  295. buf.extend_from_slice(&r.line.to_le_bytes());
  296. buf.extend_from_slice(&r.column.to_le_bytes());
  297. push_str_ref(buf, r.reference_name);
  298. push_str_ref(buf, r.candidates);
  299. push_str_ref(buf, r.from_id_str);
  300. self.ref_count += 1;
  301. }
  302. }
  303. /// One file's encoded tables, ready to hand across the JS boundary.
  304. pub struct EmitOut {
  305. pub meta: Vec<u8>,
  306. pub nodes: Vec<u8>,
  307. pub edges: Vec<u8>,
  308. pub refs: Vec<u8>,
  309. pub arena: Vec<u8>,
  310. }
  311. pub fn build_meta(t: &Tables, arena_len: u32, errors_json: StrRef, duration_ms: f64) -> Vec<u8> {
  312. let mut m = Vec::with_capacity(META_SIZE);
  313. m.push(KERNEL_ABI_VERSION);
  314. m.extend_from_slice(&[0u8; 3]);
  315. m.extend_from_slice(&t.node_count.to_le_bytes());
  316. m.extend_from_slice(&t.edge_count.to_le_bytes());
  317. m.extend_from_slice(&t.ref_count.to_le_bytes());
  318. m.extend_from_slice(&arena_len.to_le_bytes());
  319. m.extend_from_slice(&errors_json.0.to_le_bytes());
  320. m.extend_from_slice(&errors_json.1.to_le_bytes());
  321. m.extend_from_slice(&duration_ms.to_le_bytes());
  322. debug_assert_eq!(m.len(), META_SIZE);
  323. m
  324. }
  325. #[cfg(test)]
  326. mod tests {
  327. use super::*;
  328. #[test]
  329. fn row_sizes_match_constants() {
  330. let mut t = Tables::default();
  331. let mut a = Arena::default();
  332. let name = a.put("x");
  333. t.push_node(&NodeRow {
  334. kind: 0,
  335. visibility: 0,
  336. flags: BoolFlags::default(),
  337. start_line: 1,
  338. end_line: 1,
  339. start_column: 0,
  340. end_column: 0,
  341. name,
  342. qualified_name: name,
  343. id: name,
  344. docstring: NONE_STR,
  345. signature: NONE_STR,
  346. decorators: NONE_STR,
  347. type_parameters: NONE_STR,
  348. return_type: NONE_STR,
  349. extra_json: NONE_STR,
  350. });
  351. assert_eq!(t.nodes.len(), NODE_ROW_SIZE);
  352. t.push_edge(&EdgeRow {
  353. source_idx: 0,
  354. target_idx: 0,
  355. kind: 0,
  356. provenance: 0,
  357. line: NONE,
  358. column: NONE,
  359. metadata_json: NONE_STR,
  360. source_id_str: NONE_STR,
  361. target_id_str: NONE_STR,
  362. });
  363. assert_eq!(t.edges.len(), EDGE_ROW_SIZE);
  364. t.push_ref(&RefRow {
  365. from_idx: 0,
  366. kind: 1,
  367. line: 1,
  368. column: 0,
  369. reference_name: name,
  370. candidates: NONE_STR,
  371. from_id_str: NONE_STR,
  372. });
  373. assert_eq!(t.refs.len(), REF_ROW_SIZE);
  374. let meta = build_meta(&t, a.len(), NONE_STR, 0.0);
  375. assert_eq!(meta.len(), META_SIZE);
  376. }
  377. }