use super::{BTreeSet, Deserialize, Error, Language, Serialize, TypeTag};
pub const SOG_SCHEMA_VERSION: &str = "sog-v1";
pub const SEMANTIC_CANDIDATE_INDEX_VERSION: &str = "sog-candidate-index-v1";
pub const SEMANTIC_WINDOWING_VERSION: &str = "sog-windowing-v1";
pub const CROSS_LANGUAGE_CANDIDATE_INDEX_VERSION: &str = "cross-language-sog-candidate-v1";
pub const SEMANTIC_RULE_REGISTRY_VERSION: &str = "semantic-rule-registry-v1";
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OperationKind {
Source,
Filter,
Map,
Reduce,
Collect,
Validate,
PropagateError,
AcquireResource,
ReleaseResource,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FallibleKind {
Option,
Result,
}
impl FallibleKind {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Option => "option",
Self::Result => "result",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DirectPropagation {
ResultAdapter,
OptionAdapter,
}
impl DirectPropagation {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::ResultAdapter => "result_adapter",
Self::OptionAdapter => "option_adapter",
}
}
}
impl OperationKind {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Source => "source",
Self::Filter => "filter",
Self::Map => "map",
Self::Reduce => "reduce",
Self::Collect => "collect",
Self::Validate => "validate",
Self::PropagateError => "propagate_error",
Self::AcquireResource => "acquire_resource",
Self::ReleaseResource => "release_resource",
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct OperationAttributes {
pub type_tag: Option<TypeTag>,
pub api_names: BTreeSet<String>,
pub resource_kind: Option<String>,
pub fallible_kind: Option<FallibleKind>,
pub direct_propagation: Option<DirectPropagation>,
pub structure_fingerprint: Option<[u8; 16]>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OperationNode {
pub kind: OperationKind,
pub attributes: OperationAttributes,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OperationEdgeKind {
Data,
Ordering,
ResourceLifetime,
}
impl OperationEdgeKind {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Data => "data",
Self::Ordering => "ordering",
Self::ResourceLifetime => "resource_lifetime",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct OperationEdge {
pub from: u32,
pub to: u32,
pub kind: OperationEdgeKind,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SemanticOperationGraph {
pub schema_version: String,
pub language: Language,
pub build_variant_fingerprint: [u8; 32],
pub nodes: Vec<OperationNode>,
pub edges: Vec<OperationEdge>,
}
impl SemanticOperationGraph {
pub fn new(
language: Language,
build_variant_fingerprint: [u8; 32],
nodes: Vec<OperationNode>,
mut edges: Vec<OperationEdge>,
) -> Result<Self, SemanticGraphError> {
validate_nodes(&nodes)?;
edges.sort();
if edges.windows(2).any(|pair| pair[0] == pair[1]) {
return Err(SemanticGraphError::DuplicateEdge);
}
for edge in &edges {
validate_edge(&nodes, edge)?;
}
Ok(Self {
schema_version: SOG_SCHEMA_VERSION.to_owned(),
language,
build_variant_fingerprint,
nodes,
edges,
})
}
}
fn validate_nodes(nodes: &[OperationNode]) -> Result<(), SemanticGraphError> {
for (index, node) in nodes.iter().enumerate() {
let has_resource_kind = node.attributes.resource_kind.is_some();
let resource_node = matches!(
node.kind,
OperationKind::AcquireResource | OperationKind::ReleaseResource
);
if resource_node && !has_resource_kind {
return Err(SemanticGraphError::ResourceKindMissing { index });
}
if !resource_node && has_resource_kind {
return Err(SemanticGraphError::UnexpectedResourceKind { index });
}
}
Ok(())
}
fn validate_edge(nodes: &[OperationNode], edge: &OperationEdge) -> Result<(), SemanticGraphError> {
let from = usize::try_from(edge.from)
.map_err(|_| SemanticGraphError::NodeOutOfRange { index: edge.from })?;
let to = usize::try_from(edge.to)
.map_err(|_| SemanticGraphError::NodeOutOfRange { index: edge.to })?;
let Some(source) = nodes.get(from) else {
return Err(SemanticGraphError::NodeOutOfRange { index: edge.from });
};
let Some(target) = nodes.get(to) else {
return Err(SemanticGraphError::NodeOutOfRange { index: edge.to });
};
if edge.from == edge.to {
return Err(SemanticGraphError::SelfEdge { index: edge.from });
}
if edge.kind == OperationEdgeKind::ResourceLifetime
&& (source.kind != OperationKind::AcquireResource
|| target.kind != OperationKind::ReleaseResource
|| source.attributes.resource_kind != target.attributes.resource_kind)
{
return Err(SemanticGraphError::InvalidResourceLifetime);
}
Ok(())
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum SemanticGraphError {
#[error("semantic source range ends before it starts")]
InvalidSourceRange,
#[error("semantic source range count does not match graph node count")]
SourceRangeCountMismatch,
#[error("semantic graph has more nodes than local references can represent")]
GraphTooLarge,
#[error("operation index {index} is outside the graph")]
NodeOutOfRange {
index: u32,
},
#[error("operation index {index} has a self edge")]
SelfEdge {
index: u32,
},
#[error("semantic graph has a duplicate edge")]
DuplicateEdge,
#[error("resource operation at index {index} has no resource kind")]
ResourceKindMissing {
index: usize,
},
#[error("non-resource operation at index {index} has a resource kind")]
UnexpectedResourceKind {
index: usize,
},
#[error("resource lifetime edge does not join matching acquire and release operations")]
InvalidResourceLifetime,
}