Skip to main content

heddle_object_model/object/
semantic_edges.rs

1// SPDX-License-Identifier: Apache-2.0
2//! State-scoped resolved symbol edges stored as deltas over the first parent.
3
4use serde::{Deserialize, Serialize};
5
6use super::{ContentHash, SemanticIndexError};
7
8/// The relationship represented by a resolved source occurrence.
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum SemanticEdgeKind {
12    /// A non-call value reference.
13    RefersTo,
14    /// A function or method call.
15    Calls,
16    /// A type-position reference.
17    TypeRef,
18}
19
20/// A resolved occurrence-to-definition edge.
21#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
22pub struct ResolvedSemanticEdge {
23    /// Source-local occurrence id in the source file's semantic node.
24    pub source_occurrence: u32,
25    /// Repository-relative path containing the target definition.
26    pub target_path: String,
27    /// Content address of the target file's semantic node.
28    pub target_file_node: ContentHash,
29    /// Canonical index of the target definition in `SemanticFileNode::symbols`.
30    pub target_definition: u32,
31    /// Semantic relationship carried by this edge.
32    pub kind: SemanticEdgeKind,
33}
34
35/// Complete replacement edges for one source file in a binding delta.
36#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
37pub struct FileBindingDelta {
38    /// Repository-relative source path.
39    pub path: String,
40    /// Current semantic file node, or `None` when this record removes a file.
41    pub file_node: Option<ContentHash>,
42    /// Complete replacement edge list for `path`, sorted canonically.
43    pub replace_edges: Vec<ResolvedSemanticEdge>,
44}
45
46impl FileBindingDelta {
47    /// Construct a canonical replacement record.
48    pub fn new(
49        path: impl Into<String>,
50        file_node: Option<ContentHash>,
51        mut replace_edges: Vec<ResolvedSemanticEdge>,
52    ) -> Self {
53        replace_edges.sort();
54        replace_edges.dedup();
55        Self {
56            path: path.into(),
57            file_node,
58            replace_edges,
59        }
60    }
61}
62
63/// Content-addressed state binding delta.
64#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
65pub struct BindingDelta {
66    pub format_version: u8,
67    /// Content address of the first parent's binding delta.
68    pub parent: Option<ContentHash>,
69    /// Replacement records for the invalidation frontier, sorted by path.
70    pub files: Vec<FileBindingDelta>,
71}
72
73impl BindingDelta {
74    pub const FORMAT_VERSION: u8 = 1;
75
76    /// Construct a canonical binding delta.
77    pub fn new(parent: Option<ContentHash>, mut files: Vec<FileBindingDelta>) -> Self {
78        files.sort_by(|a, b| a.path.cmp(&b.path));
79        Self {
80            format_version: Self::FORMAT_VERSION,
81            parent,
82            files,
83        }
84    }
85
86    /// Encode this delta as named MessagePack.
87    pub fn encode(&self) -> Result<Vec<u8>, SemanticIndexError> {
88        rmp_serde::to_vec_named(self).map_err(|err| SemanticIndexError::Encoding(err.to_string()))
89    }
90
91    /// Decode and version-check a binding delta.
92    pub fn decode(bytes: &[u8]) -> Result<Self, SemanticIndexError> {
93        let delta: Self = rmp_serde::from_slice(bytes)
94            .map_err(|err| SemanticIndexError::Encoding(err.to_string()))?;
95        if delta.format_version != Self::FORMAT_VERSION {
96            return Err(SemanticIndexError::UnsupportedVersion(delta.format_version));
97        }
98        Ok(delta)
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    fn hash(seed: u8) -> ContentHash {
107        ContentHash::from_bytes([seed; 32])
108    }
109
110    #[test]
111    fn binding_delta_roundtrips_and_canonicalizes() {
112        let edge = ResolvedSemanticEdge {
113            source_occurrence: 3,
114            target_path: "api.rs".to_string(),
115            target_file_node: hash(2),
116            target_definition: 1,
117            kind: SemanticEdgeKind::Calls,
118        };
119        let delta = BindingDelta::new(
120            Some(hash(9)),
121            vec![
122                FileBindingDelta::new("z.rs", Some(hash(1)), vec![edge.clone(), edge]),
123                FileBindingDelta::new("a.rs", None, Vec::new()),
124            ],
125        );
126
127        assert_eq!(delta.files[0].path, "a.rs");
128        assert_eq!(delta.files[1].replace_edges.len(), 1);
129        assert_eq!(
130            BindingDelta::decode(&delta.encode().unwrap()).unwrap(),
131            delta
132        );
133    }
134}