Skip to main content

codehelion_core/semantic/
graph.rs

1use super::{BTreeSet, Deserialize, Error, Language, Serialize, TypeTag};
2
3/// Version of the closed SOG vocabulary and normalization contract.
4pub const SOG_SCHEMA_VERSION: &str = "sog-v1";
5
6/// Version of the coarse index used to bound registered SOG comparisons.
7///
8/// The index is deliberately only a candidate filter. A matching rule still
9/// checks all of its conditions after extraction, so changing this version
10/// can affect cost and recall but never turns an unchecked pair into a
11/// finding.
12pub const SEMANTIC_CANDIDATE_INDEX_VERSION: &str = "sog-candidate-index-v1";
13
14/// Version of the bounded source-window extraction for registered SOG rules.
15///
16/// Source ranges are deliberately sidecar evidence: they select the reported
17/// fragment but never enter a SOG fingerprint or a stable finding identity.
18pub const SEMANTIC_WINDOWING_VERSION: &str = "sog-windowing-v1";
19
20/// Version of the opt-in Rust-to-C++ candidate index.
21///
22/// This index is separate from ordinary semantic detection: it accepts only
23/// caller-supplied explicit comparison partitions and never joins a normal
24/// build-variant bucket.
25pub const CROSS_LANGUAGE_CANDIDATE_INDEX_VERSION: &str = "cross-language-sog-candidate-v1";
26
27/// Version of the built-in restricted-semantic rule registry.
28///
29/// This changes when the set of rule identifiers or their default enabled
30/// state changes. A scan records it beside the SOG schema so its evidence is
31/// never interpreted under a different rule selection unnoticed.
32pub const SEMANTIC_RULE_REGISTRY_VERSION: &str = "semantic-rule-registry-v1";
33
34/// One permitted semantic operation.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
36#[serde(rename_all = "snake_case")]
37pub enum OperationKind {
38    /// Read elements from a source sequence or stream.
39    Source,
40    /// Retain elements that satisfy a predicate.
41    Filter,
42    /// Transform each element independently.
43    Map,
44    /// Combine elements into one accumulated value.
45    Reduce,
46    /// Materialize elements into a collection.
47    Collect,
48    /// Check a precondition or select a valid branch.
49    Validate,
50    /// Propagate an absent or erroneous value without handling it here.
51    PropagateError,
52    /// Acquire a resource whose lifetime is tracked in this graph.
53    AcquireResource,
54    /// Release a previously acquired resource.
55    ReleaseResource,
56}
57
58/// A standard fallible container retained by a compiler-confirmed operation.
59///
60/// This closed category lets a rule distinguish `Result` error propagation
61/// from `Option` absence propagation without importing a helper's type model
62/// or guessing from syntax.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
64#[serde(rename_all = "snake_case")]
65pub enum FallibleKind {
66    /// A standard `Option` value.
67    Option,
68    /// A standard `Result` value.
69    Result,
70}
71
72impl FallibleKind {
73    /// Stable identifier used in normalized evidence.
74    #[must_use]
75    pub const fn name(self) -> &'static str {
76        match self {
77            Self::Option => "option",
78            Self::Result => "result",
79        }
80    }
81}
82
83/// A closed propagation form established by a compiler helper.
84#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
85#[serde(rename_all = "snake_case")]
86pub enum DirectPropagation {
87    /// A `Result` error is propagated while its success value is unchanged.
88    ResultAdapter,
89    /// An `Option` absence is propagated while its success value is unchanged.
90    OptionAdapter,
91}
92
93impl DirectPropagation {
94    /// Stable identifier used in normalized evidence.
95    #[must_use]
96    pub const fn name(self) -> &'static str {
97        match self {
98            Self::ResultAdapter => "result_adapter",
99            Self::OptionAdapter => "option_adapter",
100        }
101    }
102}
103
104impl OperationKind {
105    /// Stable identifier used in reports and rule definitions.
106    #[must_use]
107    pub const fn name(self) -> &'static str {
108        match self {
109            Self::Source => "source",
110            Self::Filter => "filter",
111            Self::Map => "map",
112            Self::Reduce => "reduce",
113            Self::Collect => "collect",
114            Self::Validate => "validate",
115            Self::PropagateError => "propagate_error",
116            Self::AcquireResource => "acquire_resource",
117            Self::ReleaseResource => "release_resource",
118        }
119    }
120}
121
122/// Attributes retained for an operation without importing compiler internals.
123#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
124pub struct OperationAttributes {
125    /// Resolved type category of the operated value, when available.
126    pub type_tag: Option<TypeTag>,
127    /// Compiler-resolved API names used by this operation.
128    pub api_names: BTreeSet<String>,
129    /// Registered resource category for acquire/release operations.
130    pub resource_kind: Option<String>,
131    /// Standard fallible container established for a propagation or validation
132    /// operation, when the helper schema retained it.
133    pub fallible_kind: Option<FallibleKind>,
134    /// Closed direct-propagation spelling the compiler confirmed, when any.
135    pub direct_propagation: Option<DirectPropagation>,
136    /// Position-free source structure retained by the Structural frontend.
137    ///
138    /// This keeps same-variant rules from treating different predicates or
139    /// transformations as interchangeable when their compiler-resolved API
140    /// sequence is otherwise identical. It is absent for graphs assembled by
141    /// adapters that cannot provide a source window.
142    pub structure_fingerprint: Option<[u8; 16]>,
143}
144
145/// One operation in source order; its position is a graph-local reference.
146///
147/// The position is never a stable finding identifier. Stable identifiers are
148/// minted only after a versioned normalization and rule application.
149#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
150pub struct OperationNode {
151    /// The fixed semantic operation this node represents.
152    pub kind: OperationKind,
153    /// Compiler-independent evidence used by registered rules.
154    pub attributes: OperationAttributes,
155}
156
157/// Why one operation precedes or is paired with another.
158#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
159#[serde(rename_all = "snake_case")]
160pub enum OperationEdgeKind {
161    /// A value produced by the source operation feeds the target operation.
162    Data,
163    /// Observable side effects require the source operation to precede target.
164    Ordering,
165    /// An acquire operation is paired with its corresponding release.
166    ResourceLifetime,
167}
168
169impl OperationEdgeKind {
170    /// Stable identifier used in canonical semantic evidence.
171    #[must_use]
172    pub const fn name(self) -> &'static str {
173        match self {
174            Self::Data => "data",
175            Self::Ordering => "ordering",
176            Self::ResourceLifetime => "resource_lifetime",
177        }
178    }
179}
180
181/// A directed graph edge addressed by graph-local node positions.
182#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
183pub struct OperationEdge {
184    /// Zero-based source node position.
185    pub from: u32,
186    /// Zero-based target node position.
187    pub to: u32,
188    /// The dependency relation.
189    pub kind: OperationEdgeKind,
190}
191
192/// A versioned, compiler-independent restricted semantic graph.
193#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
194pub struct SemanticOperationGraph {
195    /// Vocabulary and normalization contract version.
196    pub schema_version: String,
197    /// Language that supplied the compiler evidence.
198    pub language: Language,
199    /// Build variant that produced the compiler evidence.
200    pub build_variant_fingerprint: [u8; 32],
201    /// Operations in deterministic source order.
202    pub nodes: Vec<OperationNode>,
203    /// Canonically ordered dependencies between operations.
204    pub edges: Vec<OperationEdge>,
205}
206
207impl SemanticOperationGraph {
208    /// Construct a validated graph under the current schema version.
209    ///
210    /// Edges are sorted into one canonical order. Inputs that attempt to add a
211    /// generic operation, an invalid local reference, or an incoherent
212    /// resource pairing are rejected rather than approximated.
213    ///
214    /// # Errors
215    ///
216    /// Returns [`SemanticGraphError`] when a node attribute or edge is outside
217    /// the restricted graph contract.
218    pub fn new(
219        language: Language,
220        build_variant_fingerprint: [u8; 32],
221        nodes: Vec<OperationNode>,
222        mut edges: Vec<OperationEdge>,
223    ) -> Result<Self, SemanticGraphError> {
224        validate_nodes(&nodes)?;
225        edges.sort();
226        if edges.windows(2).any(|pair| pair[0] == pair[1]) {
227            return Err(SemanticGraphError::DuplicateEdge);
228        }
229        for edge in &edges {
230            validate_edge(&nodes, edge)?;
231        }
232        Ok(Self {
233            schema_version: SOG_SCHEMA_VERSION.to_owned(),
234            language,
235            build_variant_fingerprint,
236            nodes,
237            edges,
238        })
239    }
240}
241
242fn validate_nodes(nodes: &[OperationNode]) -> Result<(), SemanticGraphError> {
243    for (index, node) in nodes.iter().enumerate() {
244        let has_resource_kind = node.attributes.resource_kind.is_some();
245        let resource_node = matches!(
246            node.kind,
247            OperationKind::AcquireResource | OperationKind::ReleaseResource
248        );
249        if resource_node && !has_resource_kind {
250            return Err(SemanticGraphError::ResourceKindMissing { index });
251        }
252        if !resource_node && has_resource_kind {
253            return Err(SemanticGraphError::UnexpectedResourceKind { index });
254        }
255    }
256    Ok(())
257}
258
259fn validate_edge(nodes: &[OperationNode], edge: &OperationEdge) -> Result<(), SemanticGraphError> {
260    let from = usize::try_from(edge.from)
261        .map_err(|_| SemanticGraphError::NodeOutOfRange { index: edge.from })?;
262    let to = usize::try_from(edge.to)
263        .map_err(|_| SemanticGraphError::NodeOutOfRange { index: edge.to })?;
264    let Some(source) = nodes.get(from) else {
265        return Err(SemanticGraphError::NodeOutOfRange { index: edge.from });
266    };
267    let Some(target) = nodes.get(to) else {
268        return Err(SemanticGraphError::NodeOutOfRange { index: edge.to });
269    };
270    if edge.from == edge.to {
271        return Err(SemanticGraphError::SelfEdge { index: edge.from });
272    }
273    if edge.kind == OperationEdgeKind::ResourceLifetime
274        && (source.kind != OperationKind::AcquireResource
275            || target.kind != OperationKind::ReleaseResource
276            || source.attributes.resource_kind != target.attributes.resource_kind)
277    {
278        return Err(SemanticGraphError::InvalidResourceLifetime);
279    }
280    Ok(())
281}
282
283/// A graph rejected an operation or relationship outside the restricted model.
284#[derive(Debug, Error, PartialEq, Eq)]
285pub enum SemanticGraphError {
286    /// A caller supplied a source range whose end precedes its start.
287    #[error("semantic source range ends before it starts")]
288    InvalidSourceRange,
289    /// Sidecar source ranges no longer align with the normalized graph.
290    #[error("semantic source range count does not match graph node count")]
291    SourceRangeCountMismatch,
292    /// The graph cannot represent more nodes than its local references allow.
293    #[error("semantic graph has more nodes than local references can represent")]
294    GraphTooLarge,
295    /// An edge referenced no operation in this graph.
296    #[error("operation index {index} is outside the graph")]
297    NodeOutOfRange {
298        /// Graph-local node position that no node occupies.
299        index: u32,
300    },
301    /// An operation cannot depend on itself.
302    #[error("operation index {index} has a self edge")]
303    SelfEdge {
304        /// Graph-local node position used as both endpoints.
305        index: u32,
306    },
307    /// The same edge was supplied more than once.
308    #[error("semantic graph has a duplicate edge")]
309    DuplicateEdge,
310    /// An acquire or release omitted the category required to pair it safely.
311    #[error("resource operation at index {index} has no resource kind")]
312    ResourceKindMissing {
313        /// Graph-local node position of the incomplete resource operation.
314        index: usize,
315    },
316    /// A non-resource operation attempted to carry a resource category.
317    #[error("non-resource operation at index {index} has a resource kind")]
318    UnexpectedResourceKind {
319        /// Graph-local node position carrying unsupported resource metadata.
320        index: usize,
321    },
322    /// A resource edge must pair matching acquire and release operations.
323    #[error("resource lifetime edge does not join matching acquire and release operations")]
324    InvalidResourceLifetime,
325}