use crate::engine::Evaluation;
use crate::source::{SourceFile, Span};
use crate::value::Value;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
pub const PROVENANCE_SCHEMA_VERSION: u32 = 1;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProvenanceEvaluation {
pub evaluation: Evaluation,
pub provenance: ProvenanceGraph,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProvenanceGraph {
pub schema_version: u32,
pub module: String,
pub query: String,
pub result_node: String,
pub nodes: Vec<ProvenanceNode>,
pub edges: Vec<ProvenanceEdge>,
}
impl ProvenanceGraph {
#[must_use]
pub fn node(&self, id: &str) -> Option<&ProvenanceNode> {
self.nodes.iter().find(|node| node.id == id)
}
pub fn incoming<'a>(&'a self, id: &'a str) -> impl Iterator<Item = &'a ProvenanceEdge> + 'a {
self.edges.iter().filter(move |edge| edge.to == id)
}
pub fn outgoing<'a>(&'a self, id: &'a str) -> impl Iterator<Item = &'a ProvenanceEdge> + 'a {
self.edges.iter().filter(move |edge| edge.from == id)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProvenanceNode {
pub id: String,
pub kind: ProvenanceNodeKind,
pub label: String,
pub status: ProvenanceStatus,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub values: Vec<Value>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub missing: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub reasons: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<ProvenanceLocation>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProvenanceNodeKind {
InputField,
Literal,
Name,
FieldAccess,
DeriveCall,
BuiltinCall,
UnaryOperation,
BinaryOperation,
RuleCondition,
DecisionCandidate,
Override,
Suppression,
DecisionResult,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProvenanceStatus {
Known,
Entity,
True,
False,
Unknown,
Conflict,
Applied,
Blocked,
Suppressed,
Resolved,
Undefined,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct ProvenanceEdge {
pub from: String,
pub to: String,
pub kind: ProvenanceEdgeKind,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProvenanceEdgeKind {
Operand,
Reads,
Argument,
Evaluates,
Enables,
Produces,
Suppresses,
Contributes,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProvenanceLocation {
pub file: String,
pub line: usize,
pub column: usize,
pub end_line: usize,
pub end_column: usize,
pub expression: String,
}
#[derive(Clone, Debug)]
pub(crate) struct ProvenanceObservation {
pub(crate) status: ProvenanceStatus,
pub(crate) values: Vec<Value>,
pub(crate) missing: Vec<String>,
pub(crate) reasons: Vec<String>,
}
impl ProvenanceObservation {
pub(crate) fn status(status: ProvenanceStatus) -> Self {
Self {
status,
values: Vec::new(),
missing: Vec::new(),
reasons: Vec::new(),
}
}
pub(crate) fn value(status: ProvenanceStatus, value: Value) -> Self {
Self {
status,
values: vec![value],
missing: Vec::new(),
reasons: Vec::new(),
}
}
}
#[derive(Clone, Debug)]
struct ExpressionFrame {
id: String,
kind: ProvenanceNodeKind,
label: String,
span: Span,
next_child: usize,
dependencies: BTreeSet<(String, ProvenanceEdgeKind)>,
}
#[derive(Clone, Debug)]
pub(crate) struct ProvenanceRecorder {
source: SourceFile,
module: String,
query: String,
scope: String,
next_root: usize,
frames: Vec<ExpressionFrame>,
nodes: BTreeMap<String, ProvenanceNode>,
edges: BTreeSet<ProvenanceEdge>,
last_completed: Option<String>,
}
impl ProvenanceRecorder {
pub(crate) fn new(source: SourceFile, module: String, query: String) -> Self {
Self {
source,
module,
query,
scope: String::new(),
next_root: 0,
frames: Vec::new(),
nodes: BTreeMap::new(),
edges: BTreeSet::new(),
last_completed: None,
}
}
pub(crate) fn set_scope(&mut self, scope: impl Into<String>) {
debug_assert!(self.frames.is_empty());
self.scope = scope.into();
self.next_root = 0;
self.last_completed = None;
}
pub(crate) fn begin_expression(&mut self, kind: ProvenanceNodeKind, label: String, span: Span) {
let id = if let Some(parent) = self.frames.last_mut() {
let child = parent.next_child;
parent.next_child += 1;
format!("{}/expression:{child}", parent.id)
} else {
let root = self.next_root;
self.next_root += 1;
if root == 0 {
format!("expression:{}", self.scope)
} else {
format!("expression:{}/root:{root}", self.scope)
}
};
self.frames.push(ExpressionFrame {
id,
kind,
label,
span,
next_child: 0,
dependencies: BTreeSet::new(),
});
}
pub(crate) fn finish_expression(&mut self, observation: ProvenanceObservation) {
let frame = self
.frames
.pop()
.expect("a provenance expression frame must be active");
let id = frame.id.clone();
self.insert_node(ProvenanceNode {
id: id.clone(),
kind: frame.kind,
label: frame.label,
status: observation.status,
values: observation.values,
missing: sorted_unique(observation.missing),
reasons: sorted_unique(observation.reasons),
location: Some(self.location(frame.span)),
});
for (dependency, kind) in frame.dependencies {
self.connect(dependency, id.clone(), kind);
}
if let Some(parent) = self.frames.last_mut() {
parent
.dependencies
.insert((id.clone(), ProvenanceEdgeKind::Operand));
}
self.last_completed = Some(id);
}
pub(crate) fn record_dependency(&mut self, from: String, kind: ProvenanceEdgeKind) {
if let Some(frame) = self.frames.last_mut() {
frame.dependencies.insert((from, kind));
}
}
pub(crate) fn record_input(
&mut self,
binding: &str,
field: &str,
observation: ProvenanceObservation,
) -> String {
let path = format!("{binding}.{field}");
let id = format!("input:{path}");
self.insert_node(ProvenanceNode {
id: id.clone(),
kind: ProvenanceNodeKind::InputField,
label: path,
status: observation.status,
values: observation.values,
missing: sorted_unique(observation.missing),
reasons: sorted_unique(observation.reasons),
location: None,
});
self.record_dependency(id.clone(), ProvenanceEdgeKind::Reads);
id
}
pub(crate) fn record_node(
&mut self,
id: impl Into<String>,
kind: ProvenanceNodeKind,
label: impl Into<String>,
observation: ProvenanceObservation,
) -> String {
let id = id.into();
self.insert_node(ProvenanceNode {
id: id.clone(),
kind,
label: label.into(),
status: observation.status,
values: observation.values,
missing: sorted_unique(observation.missing),
reasons: sorted_unique(observation.reasons),
location: None,
});
id
}
pub(crate) fn connect(
&mut self,
from: impl Into<String>,
to: impl Into<String>,
kind: ProvenanceEdgeKind,
) {
self.edges.insert(ProvenanceEdge {
from: from.into(),
to: to.into(),
kind,
});
}
pub(crate) fn last_completed(&self) -> Option<String> {
self.last_completed.clone()
}
pub(crate) fn finish(self, result_node: String) -> ProvenanceGraph {
debug_assert!(self.frames.is_empty());
ProvenanceGraph {
schema_version: PROVENANCE_SCHEMA_VERSION,
module: self.module,
query: self.query,
result_node,
nodes: self.nodes.into_values().collect(),
edges: self.edges.into_iter().collect(),
}
}
fn insert_node(&mut self, node: ProvenanceNode) {
match self.nodes.get(&node.id) {
Some(existing) => debug_assert_eq!(existing, &node),
None => {
self.nodes.insert(node.id.clone(), node);
}
}
}
fn location(&self, span: Span) -> ProvenanceLocation {
let (line, column) = self.source.line_col(span.start);
let (end_line, end_column) = self.source.line_col(span.end);
ProvenanceLocation {
file: self.source.name_at(span.start).to_owned(),
line,
column,
end_line,
end_column,
expression: self
.source
.text
.get(span.start..span.end)
.unwrap_or_default()
.trim()
.to_owned(),
}
}
}
fn sorted_unique(values: Vec<String>) -> Vec<String> {
values
.into_iter()
.collect::<BTreeSet<_>>()
.into_iter()
.collect()
}