1use std::collections::BTreeMap;
29
30use serde::{Deserialize, Serialize};
31
32use super::ContentHash;
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
45#[serde(rename_all = "snake_case")]
46pub enum SymbolKindTag {
47 Function,
49 Type,
51 Enum,
53 Trait,
55 Class,
57 Interface,
59 TypeAlias,
61 Const,
63 Module,
65 Other,
67}
68
69impl SymbolKindTag {
70 pub fn tag_byte(self) -> u8 {
73 match self {
74 SymbolKindTag::Function => 1,
75 SymbolKindTag::Type => 2,
76 SymbolKindTag::Enum => 3,
77 SymbolKindTag::Trait => 4,
78 SymbolKindTag::Class => 5,
79 SymbolKindTag::Interface => 6,
80 SymbolKindTag::TypeAlias => 7,
81 SymbolKindTag::Const => 8,
82 SymbolKindTag::Module => 9,
83 SymbolKindTag::Other => 10,
84 }
85 }
86}
87
88#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
90#[serde(rename_all = "snake_case")]
91pub enum SemanticEntryKind {
92 Dir,
94 File,
96 Opaque,
101}
102
103impl SemanticEntryKind {
104 pub fn tag_byte(self) -> u8 {
106 match self {
107 SemanticEntryKind::Dir => 1,
108 SemanticEntryKind::File => 2,
109 SemanticEntryKind::Opaque => 3,
110 }
111 }
112}
113
114#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
116pub struct SymbolEntry {
117 pub name: String,
119 pub kind: SymbolKindTag,
121 pub container_path: Vec<String>,
123 pub semantic_hash: ContentHash,
127 pub span: (u32, u32),
131}
132
133impl SymbolEntry {
134 pub fn address(&self) -> String {
137 if self.container_path.is_empty() {
138 self.name.clone()
139 } else {
140 format!("{}::{}", self.container_path.join("::"), self.name)
141 }
142 }
143
144 fn sort_key(&self) -> (&[String], &str, u8, u32) {
146 (
147 &self.container_path,
148 self.name.as_str(),
149 self.kind.tag_byte(),
150 self.span.0,
151 )
152 }
153}
154
155pub fn compute_symbol_semantic_hash(kind: SymbolKindTag, token_stream: &[u8]) -> ContentHash {
166 let mut buf = Vec::with_capacity(2 + token_stream.len());
167 buf.push(kind.tag_byte());
168 buf.push(0x00);
169 buf.extend_from_slice(token_stream);
170 ContentHash::compute_typed("hd-sem-sym-v1", &buf)
171}
172
173pub fn compute_file_scaffold_hash(token_stream: &[u8]) -> ContentHash {
183 ContentHash::compute_typed("hd-sem-scaffold-v1", token_stream)
184}
185
186pub fn compute_file_semantic_digest(
197 scaffold_hash: ContentHash,
198 symbols: &[SymbolEntry],
199) -> ContentHash {
200 let mut buf = Vec::new();
201 buf.extend_from_slice(scaffold_hash.as_bytes());
202 for symbol in symbols {
203 buf.extend_from_slice(&(symbol.container_path.len() as u32).to_le_bytes());
204 for segment in &symbol.container_path {
205 buf.extend_from_slice(&(segment.len() as u32).to_le_bytes());
206 buf.extend_from_slice(segment.as_bytes());
207 }
208 buf.extend_from_slice(&(symbol.name.len() as u32).to_le_bytes());
209 buf.extend_from_slice(symbol.name.as_bytes());
210 buf.push(symbol.kind.tag_byte());
211 buf.extend_from_slice(symbol.semantic_hash.as_bytes());
212 }
213 ContentHash::compute_typed("hd-sem-file-v2", &buf)
214}
215
216pub fn compute_dir_semantic_digest(entries: &[SemanticTreeEntry]) -> ContentHash {
222 let mut buf = Vec::new();
223 for entry in entries {
224 buf.extend_from_slice(&(entry.name.len() as u32).to_le_bytes());
225 buf.extend_from_slice(entry.name.as_bytes());
226 buf.push(entry.kind.tag_byte());
227 buf.extend_from_slice(entry.semantic_digest.as_bytes());
228 }
229 ContentHash::compute_typed("hd-sem-dir-v2", &buf)
230}
231
232#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
235pub struct SemanticFileNode {
236 pub format_version: u8,
237 pub language: String,
238 pub grammar_version: String,
239 pub extractor_version: u32,
240 pub source_blob: ContentHash,
242 pub scaffold_hash: ContentHash,
246 pub symbols: Vec<SymbolEntry>,
248 pub semantic_digest: ContentHash,
250}
251
252impl SemanticFileNode {
253 pub const FORMAT_VERSION: u8 = 1;
254
255 pub fn new(
258 language: impl Into<String>,
259 grammar_version: impl Into<String>,
260 extractor_version: u32,
261 source_blob: ContentHash,
262 scaffold_hash: ContentHash,
263 mut symbols: Vec<SymbolEntry>,
264 ) -> Self {
265 symbols.sort_by(|a, b| a.sort_key().cmp(&b.sort_key()));
266 let semantic_digest = compute_file_semantic_digest(scaffold_hash, &symbols);
267 Self {
268 format_version: Self::FORMAT_VERSION,
269 language: language.into(),
270 grammar_version: grammar_version.into(),
271 extractor_version,
272 source_blob,
273 scaffold_hash,
274 symbols,
275 semantic_digest,
276 }
277 }
278
279 pub fn encode(&self) -> Result<Vec<u8>, SemanticIndexError> {
280 rmp_serde::to_vec_named(self).map_err(|err| SemanticIndexError::Encoding(err.to_string()))
281 }
282
283 pub fn decode(bytes: &[u8]) -> Result<Self, SemanticIndexError> {
284 let node: Self =
285 rmp_serde::from_slice(bytes).map_err(|err| SemanticIndexError::Encoding(err.to_string()))?;
286 if node.format_version != Self::FORMAT_VERSION {
287 return Err(SemanticIndexError::UnsupportedVersion(node.format_version));
288 }
289 Ok(node)
290 }
291
292 pub fn symbol_by_address(&self, address: &str) -> Option<&SymbolEntry> {
294 self.symbols.iter().find(|s| s.address() == address)
295 }
296}
297
298#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
300pub struct SemanticTreeEntry {
301 pub name: String,
302 pub kind: SemanticEntryKind,
303 pub node: ContentHash,
307 pub semantic_digest: ContentHash,
309}
310
311#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
313pub struct SemanticTreeNode {
314 pub format_version: u8,
315 pub entries: Vec<SemanticTreeEntry>,
317}
318
319impl SemanticTreeNode {
320 pub const FORMAT_VERSION: u8 = 1;
321
322 pub fn new(mut entries: Vec<SemanticTreeEntry>) -> (Self, ContentHash) {
325 entries.sort_by(|a, b| a.name.cmp(&b.name));
326 let digest = compute_dir_semantic_digest(&entries);
327 (
328 Self {
329 format_version: Self::FORMAT_VERSION,
330 entries,
331 },
332 digest,
333 )
334 }
335
336 pub fn semantic_digest(&self) -> ContentHash {
338 compute_dir_semantic_digest(&self.entries)
339 }
340
341 pub fn encode(&self) -> Result<Vec<u8>, SemanticIndexError> {
342 rmp_serde::to_vec_named(self).map_err(|err| SemanticIndexError::Encoding(err.to_string()))
343 }
344
345 pub fn decode(bytes: &[u8]) -> Result<Self, SemanticIndexError> {
346 let node: Self =
347 rmp_serde::from_slice(bytes).map_err(|err| SemanticIndexError::Encoding(err.to_string()))?;
348 if node.format_version != Self::FORMAT_VERSION {
349 return Err(SemanticIndexError::UnsupportedVersion(node.format_version));
350 }
351 Ok(node)
352 }
353
354 pub fn get(&self, name: &str) -> Option<&SemanticTreeEntry> {
355 self.entries
356 .binary_search_by(|e| e.name.as_str().cmp(name))
357 .ok()
358 .map(|i| &self.entries[i])
359 }
360}
361
362#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
365pub struct SemanticIndexRoot {
366 pub format_version: u8,
367 pub extractor_version: u32,
368 pub grammars: BTreeMap<String, String>,
370 pub tree: ContentHash,
372 pub semantic_digest: ContentHash,
374}
375
376impl SemanticIndexRoot {
377 pub const FORMAT_VERSION: u8 = 1;
378
379 pub fn new(
380 extractor_version: u32,
381 grammars: BTreeMap<String, String>,
382 tree: ContentHash,
383 semantic_digest: ContentHash,
384 ) -> Self {
385 Self {
386 format_version: Self::FORMAT_VERSION,
387 extractor_version,
388 grammars,
389 tree,
390 semantic_digest,
391 }
392 }
393
394 pub fn encode(&self) -> Result<Vec<u8>, SemanticIndexError> {
395 rmp_serde::to_vec_named(self).map_err(|err| SemanticIndexError::Encoding(err.to_string()))
396 }
397
398 pub fn decode(bytes: &[u8]) -> Result<Self, SemanticIndexError> {
399 let root: Self =
400 rmp_serde::from_slice(bytes).map_err(|err| SemanticIndexError::Encoding(err.to_string()))?;
401 if root.format_version != Self::FORMAT_VERSION {
402 return Err(SemanticIndexError::UnsupportedVersion(root.format_version));
403 }
404 Ok(root)
405 }
406}
407
408#[derive(Debug, thiserror::Error)]
409pub enum SemanticIndexError {
410 #[error("unsupported semantic index node version {0}")]
411 UnsupportedVersion(u8),
412 #[error("semantic index node encoding error: {0}")]
413 Encoding(String),
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419
420 fn h(seed: u8) -> ContentHash {
421 ContentHash::from_bytes([seed; 32])
422 }
423
424 fn sym(name: &str, container: &[&str], kind: SymbolKindTag, span: (u32, u32)) -> SymbolEntry {
425 SymbolEntry {
426 name: name.to_string(),
427 kind,
428 container_path: container.iter().map(|s| s.to_string()).collect(),
429 semantic_hash: ContentHash::compute(name.as_bytes()),
430 span,
431 }
432 }
433
434 #[test]
435 fn file_digest_excludes_span() {
436 let a = SemanticFileNode::new(
437 "rust",
438 "0.24",
439 1,
440 h(1),
441 h(0),
442 vec![sym("foo", &[], SymbolKindTag::Function, (10, 20))],
443 );
444 let b = SemanticFileNode::new(
446 "rust",
447 "0.24",
448 1,
449 h(1),
450 h(0),
451 vec![sym("foo", &[], SymbolKindTag::Function, (99, 120))],
452 );
453 assert_eq!(
454 a.semantic_digest, b.semantic_digest,
455 "span must not affect the file semantic_digest"
456 );
457 }
458
459 #[test]
460 fn file_digest_changes_on_symbol_hash_change() {
461 let mut s = sym("foo", &[], SymbolKindTag::Function, (1, 2));
462 let d1 = compute_file_semantic_digest(h(0), std::slice::from_ref(&s));
463 s.semantic_hash = ContentHash::compute(b"different-body");
464 let d2 = compute_file_semantic_digest(h(0), std::slice::from_ref(&s));
465 assert_ne!(d1, d2);
466 }
467
468 #[test]
469 fn file_digest_changes_on_scaffold_change() {
470 let syms = [sym("foo", &[], SymbolKindTag::Function, (1, 2))];
471 let d1 = compute_file_semantic_digest(compute_file_scaffold_hash(b"use a;"), &syms);
472 let d2 = compute_file_semantic_digest(compute_file_scaffold_hash(b"use b;"), &syms);
473 assert_ne!(
474 d1, d2,
475 "scaffold (non-definition top-level tokens) must affect the file digest"
476 );
477 }
478
479 #[test]
480 fn file_digest_framing_is_unambiguous() {
481 let one = sym("f", &["a::b"], SymbolKindTag::Function, (0, 0));
484 let two = sym("f", &["a", "b"], SymbolKindTag::Function, (0, 0));
485 assert_ne!(
486 compute_file_semantic_digest(h(0), &[one]),
487 compute_file_semantic_digest(h(0), &[two]),
488 );
489 }
490
491 #[test]
492 fn symbol_hash_stable_and_kind_sensitive() {
493 let ts = b"some token stream";
494 let a = compute_symbol_semantic_hash(SymbolKindTag::Function, ts);
495 let b = compute_symbol_semantic_hash(SymbolKindTag::Function, ts);
496 assert_eq!(a, b);
497 let c = compute_symbol_semantic_hash(SymbolKindTag::Type, ts);
498 assert_ne!(a, c, "kind participates in the symbol hash");
499 }
500
501 #[test]
502 fn symbols_sorted_canonically() {
503 let node = SemanticFileNode::new(
504 "rust",
505 "0.24",
506 1,
507 h(1),
508 h(0),
509 vec![
510 sym("zed", &[], SymbolKindTag::Function, (1, 1)),
511 sym("abe", &["Impl"], SymbolKindTag::Function, (2, 2)),
512 sym("abe", &[], SymbolKindTag::Function, (3, 3)),
513 ],
514 );
515 let names: Vec<_> = node.symbols.iter().map(|s| s.address()).collect();
516 assert_eq!(names, vec!["abe", "zed", "Impl::abe"]);
517 }
518
519 #[test]
520 fn dir_digest_stable_and_roundtrip() {
521 let e = SemanticTreeEntry {
522 name: "a.rs".to_string(),
523 kind: SemanticEntryKind::File,
524 node: h(5),
525 semantic_digest: h(6),
526 };
527 let (node, digest) = SemanticTreeNode::new(vec![e.clone()]);
528 assert_eq!(node.semantic_digest(), digest);
529 let bytes = node.encode().unwrap();
530 assert_eq!(SemanticTreeNode::decode(&bytes).unwrap(), node);
531 }
532
533 #[test]
534 fn address_spelling() {
535 assert_eq!(sym("foo", &[], SymbolKindTag::Function, (0, 0)).address(), "foo");
536 assert_eq!(
537 sym("open", &["Repository"], SymbolKindTag::Function, (0, 0)).address(),
538 "Repository::open"
539 );
540 }
541}