use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use super::ContentHash;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SymbolKindTag {
Function,
Type,
Enum,
Trait,
Class,
Interface,
TypeAlias,
Const,
Module,
Other,
}
impl SymbolKindTag {
pub fn tag_byte(self) -> u8 {
match self {
SymbolKindTag::Function => 1,
SymbolKindTag::Type => 2,
SymbolKindTag::Enum => 3,
SymbolKindTag::Trait => 4,
SymbolKindTag::Class => 5,
SymbolKindTag::Interface => 6,
SymbolKindTag::TypeAlias => 7,
SymbolKindTag::Const => 8,
SymbolKindTag::Module => 9,
SymbolKindTag::Other => 10,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SemanticEntryKind {
Dir,
File,
Opaque,
}
impl SemanticEntryKind {
pub fn tag_byte(self) -> u8 {
match self {
SemanticEntryKind::Dir => 1,
SemanticEntryKind::File => 2,
SemanticEntryKind::Opaque => 3,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SymbolEntry {
pub name: String,
pub kind: SymbolKindTag,
pub container_path: Vec<String>,
pub semantic_hash: ContentHash,
pub span: (u32, u32),
}
impl SymbolEntry {
pub fn address(&self) -> String {
if self.container_path.is_empty() {
self.name.clone()
} else {
format!("{}::{}", self.container_path.join("::"), self.name)
}
}
fn sort_key(&self) -> (&[String], &str, u8, u32) {
(
&self.container_path,
self.name.as_str(),
self.kind.tag_byte(),
self.span.0,
)
}
}
pub fn compute_symbol_semantic_hash(kind: SymbolKindTag, token_stream: &[u8]) -> ContentHash {
let mut buf = Vec::with_capacity(2 + token_stream.len());
buf.push(kind.tag_byte());
buf.push(0x00);
buf.extend_from_slice(token_stream);
ContentHash::compute_typed("hd-sem-sym-v1", &buf)
}
pub fn compute_file_scaffold_hash(token_stream: &[u8]) -> ContentHash {
ContentHash::compute_typed("hd-sem-scaffold-v1", token_stream)
}
pub fn compute_file_semantic_digest(
scaffold_hash: ContentHash,
symbols: &[SymbolEntry],
) -> ContentHash {
let mut buf = Vec::new();
buf.extend_from_slice(scaffold_hash.as_bytes());
for symbol in symbols {
buf.extend_from_slice(&(symbol.container_path.len() as u32).to_le_bytes());
for segment in &symbol.container_path {
buf.extend_from_slice(&(segment.len() as u32).to_le_bytes());
buf.extend_from_slice(segment.as_bytes());
}
buf.extend_from_slice(&(symbol.name.len() as u32).to_le_bytes());
buf.extend_from_slice(symbol.name.as_bytes());
buf.push(symbol.kind.tag_byte());
buf.extend_from_slice(symbol.semantic_hash.as_bytes());
}
ContentHash::compute_typed("hd-sem-file-v2", &buf)
}
pub fn compute_dir_semantic_digest(entries: &[SemanticTreeEntry]) -> ContentHash {
let mut buf = Vec::new();
for entry in entries {
buf.extend_from_slice(&(entry.name.len() as u32).to_le_bytes());
buf.extend_from_slice(entry.name.as_bytes());
buf.push(entry.kind.tag_byte());
buf.extend_from_slice(entry.semantic_digest.as_bytes());
}
ContentHash::compute_typed("hd-sem-dir-v2", &buf)
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SemanticFileNode {
pub format_version: u8,
pub language: String,
pub grammar_version: String,
pub extractor_version: u32,
pub source_blob: ContentHash,
pub scaffold_hash: ContentHash,
pub symbols: Vec<SymbolEntry>,
pub semantic_digest: ContentHash,
}
impl SemanticFileNode {
pub const FORMAT_VERSION: u8 = 1;
pub fn new(
language: impl Into<String>,
grammar_version: impl Into<String>,
extractor_version: u32,
source_blob: ContentHash,
scaffold_hash: ContentHash,
mut symbols: Vec<SymbolEntry>,
) -> Self {
symbols.sort_by(|a, b| a.sort_key().cmp(&b.sort_key()));
let semantic_digest = compute_file_semantic_digest(scaffold_hash, &symbols);
Self {
format_version: Self::FORMAT_VERSION,
language: language.into(),
grammar_version: grammar_version.into(),
extractor_version,
source_blob,
scaffold_hash,
symbols,
semantic_digest,
}
}
pub fn encode(&self) -> Result<Vec<u8>, SemanticIndexError> {
rmp_serde::to_vec_named(self).map_err(|err| SemanticIndexError::Encoding(err.to_string()))
}
pub fn decode(bytes: &[u8]) -> Result<Self, SemanticIndexError> {
let node: Self =
rmp_serde::from_slice(bytes).map_err(|err| SemanticIndexError::Encoding(err.to_string()))?;
if node.format_version != Self::FORMAT_VERSION {
return Err(SemanticIndexError::UnsupportedVersion(node.format_version));
}
Ok(node)
}
pub fn symbol_by_address(&self, address: &str) -> Option<&SymbolEntry> {
self.symbols.iter().find(|s| s.address() == address)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SemanticTreeEntry {
pub name: String,
pub kind: SemanticEntryKind,
pub node: ContentHash,
pub semantic_digest: ContentHash,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SemanticTreeNode {
pub format_version: u8,
pub entries: Vec<SemanticTreeEntry>,
}
impl SemanticTreeNode {
pub const FORMAT_VERSION: u8 = 1;
pub fn new(mut entries: Vec<SemanticTreeEntry>) -> (Self, ContentHash) {
entries.sort_by(|a, b| a.name.cmp(&b.name));
let digest = compute_dir_semantic_digest(&entries);
(
Self {
format_version: Self::FORMAT_VERSION,
entries,
},
digest,
)
}
pub fn semantic_digest(&self) -> ContentHash {
compute_dir_semantic_digest(&self.entries)
}
pub fn encode(&self) -> Result<Vec<u8>, SemanticIndexError> {
rmp_serde::to_vec_named(self).map_err(|err| SemanticIndexError::Encoding(err.to_string()))
}
pub fn decode(bytes: &[u8]) -> Result<Self, SemanticIndexError> {
let node: Self =
rmp_serde::from_slice(bytes).map_err(|err| SemanticIndexError::Encoding(err.to_string()))?;
if node.format_version != Self::FORMAT_VERSION {
return Err(SemanticIndexError::UnsupportedVersion(node.format_version));
}
Ok(node)
}
pub fn get(&self, name: &str) -> Option<&SemanticTreeEntry> {
self.entries
.binary_search_by(|e| e.name.as_str().cmp(name))
.ok()
.map(|i| &self.entries[i])
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SemanticIndexRoot {
pub format_version: u8,
pub extractor_version: u32,
pub grammars: BTreeMap<String, String>,
pub tree: ContentHash,
pub semantic_digest: ContentHash,
}
impl SemanticIndexRoot {
pub const FORMAT_VERSION: u8 = 1;
pub fn new(
extractor_version: u32,
grammars: BTreeMap<String, String>,
tree: ContentHash,
semantic_digest: ContentHash,
) -> Self {
Self {
format_version: Self::FORMAT_VERSION,
extractor_version,
grammars,
tree,
semantic_digest,
}
}
pub fn encode(&self) -> Result<Vec<u8>, SemanticIndexError> {
rmp_serde::to_vec_named(self).map_err(|err| SemanticIndexError::Encoding(err.to_string()))
}
pub fn decode(bytes: &[u8]) -> Result<Self, SemanticIndexError> {
let root: Self =
rmp_serde::from_slice(bytes).map_err(|err| SemanticIndexError::Encoding(err.to_string()))?;
if root.format_version != Self::FORMAT_VERSION {
return Err(SemanticIndexError::UnsupportedVersion(root.format_version));
}
Ok(root)
}
}
#[derive(Debug, thiserror::Error)]
pub enum SemanticIndexError {
#[error("unsupported semantic index node version {0}")]
UnsupportedVersion(u8),
#[error("semantic index node encoding error: {0}")]
Encoding(String),
}
#[cfg(test)]
mod tests {
use super::*;
fn h(seed: u8) -> ContentHash {
ContentHash::from_bytes([seed; 32])
}
fn sym(name: &str, container: &[&str], kind: SymbolKindTag, span: (u32, u32)) -> SymbolEntry {
SymbolEntry {
name: name.to_string(),
kind,
container_path: container.iter().map(|s| s.to_string()).collect(),
semantic_hash: ContentHash::compute(name.as_bytes()),
span,
}
}
#[test]
fn file_digest_excludes_span() {
let a = SemanticFileNode::new(
"rust",
"0.24",
1,
h(1),
h(0),
vec![sym("foo", &[], SymbolKindTag::Function, (10, 20))],
);
let b = SemanticFileNode::new(
"rust",
"0.24",
1,
h(1),
h(0),
vec![sym("foo", &[], SymbolKindTag::Function, (99, 120))],
);
assert_eq!(
a.semantic_digest, b.semantic_digest,
"span must not affect the file semantic_digest"
);
}
#[test]
fn file_digest_changes_on_symbol_hash_change() {
let mut s = sym("foo", &[], SymbolKindTag::Function, (1, 2));
let d1 = compute_file_semantic_digest(h(0), std::slice::from_ref(&s));
s.semantic_hash = ContentHash::compute(b"different-body");
let d2 = compute_file_semantic_digest(h(0), std::slice::from_ref(&s));
assert_ne!(d1, d2);
}
#[test]
fn file_digest_changes_on_scaffold_change() {
let syms = [sym("foo", &[], SymbolKindTag::Function, (1, 2))];
let d1 = compute_file_semantic_digest(compute_file_scaffold_hash(b"use a;"), &syms);
let d2 = compute_file_semantic_digest(compute_file_scaffold_hash(b"use b;"), &syms);
assert_ne!(
d1, d2,
"scaffold (non-definition top-level tokens) must affect the file digest"
);
}
#[test]
fn file_digest_framing_is_unambiguous() {
let one = sym("f", &["a::b"], SymbolKindTag::Function, (0, 0));
let two = sym("f", &["a", "b"], SymbolKindTag::Function, (0, 0));
assert_ne!(
compute_file_semantic_digest(h(0), &[one]),
compute_file_semantic_digest(h(0), &[two]),
);
}
#[test]
fn symbol_hash_stable_and_kind_sensitive() {
let ts = b"some token stream";
let a = compute_symbol_semantic_hash(SymbolKindTag::Function, ts);
let b = compute_symbol_semantic_hash(SymbolKindTag::Function, ts);
assert_eq!(a, b);
let c = compute_symbol_semantic_hash(SymbolKindTag::Type, ts);
assert_ne!(a, c, "kind participates in the symbol hash");
}
#[test]
fn symbols_sorted_canonically() {
let node = SemanticFileNode::new(
"rust",
"0.24",
1,
h(1),
h(0),
vec![
sym("zed", &[], SymbolKindTag::Function, (1, 1)),
sym("abe", &["Impl"], SymbolKindTag::Function, (2, 2)),
sym("abe", &[], SymbolKindTag::Function, (3, 3)),
],
);
let names: Vec<_> = node.symbols.iter().map(|s| s.address()).collect();
assert_eq!(names, vec!["abe", "zed", "Impl::abe"]);
}
#[test]
fn dir_digest_stable_and_roundtrip() {
let e = SemanticTreeEntry {
name: "a.rs".to_string(),
kind: SemanticEntryKind::File,
node: h(5),
semantic_digest: h(6),
};
let (node, digest) = SemanticTreeNode::new(vec![e.clone()]);
assert_eq!(node.semantic_digest(), digest);
let bytes = node.encode().unwrap();
assert_eq!(SemanticTreeNode::decode(&bytes).unwrap(), node);
}
#[test]
fn address_spelling() {
assert_eq!(sym("foo", &[], SymbolKindTag::Function, (0, 0)).address(), "foo");
assert_eq!(
sym("open", &["Repository"], SymbolKindTag::Function, (0, 0)).address(),
"Repository::open"
);
}
}