use std::collections::HashMap;
use panproto_gat::{Equation, Term, Theory};
use panproto_inst::functor::FInstance;
use panproto_inst::value::Value;
use panproto_schema::Schema;
#[derive(Clone, Debug)]
pub struct EmbeddedDependency {
pub pattern_vertex: String,
pub pattern_values: HashMap<String, Value>,
pub consequence_vertex: String,
pub consequence_values: HashMap<String, Value>,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ChaseError {
#[error("chase did not terminate after {0} iterations")]
NonTermination(usize),
}
fn row_matches(row: &HashMap<String, Value>, required: &HashMap<String, Value>) -> bool {
required
.iter()
.all(|(col, val)| row.get(col).is_some_and(|v| v == val))
}
fn table_contains_match(
rows: &[HashMap<String, Value>],
required: &HashMap<String, Value>,
) -> bool {
rows.iter().any(|row| row_matches(row, required))
}
pub fn chase_functor(
instance: &FInstance,
dependencies: &[EmbeddedDependency],
max_iterations: usize,
) -> Result<FInstance, ChaseError> {
let mut result = instance.clone();
for _ in 0..max_iterations {
let mut changed = false;
for dep in dependencies {
let pattern_rows: Vec<HashMap<String, Value>> = result
.tables
.get(&dep.pattern_vertex)
.cloned()
.unwrap_or_default();
for row in &pattern_rows {
if !row_matches(row, &dep.pattern_values) {
continue;
}
let consequence_rows = result
.tables
.entry(dep.consequence_vertex.clone())
.or_default();
if !table_contains_match(consequence_rows, &dep.consequence_values) {
consequence_rows.push(dep.consequence_values.clone());
changed = true;
}
}
}
if !changed {
return Ok(result);
}
}
Err(ChaseError::NonTermination(max_iterations))
}
#[must_use]
pub fn dependencies_from_schema(schema: &Schema) -> Vec<EmbeddedDependency> {
let mut deps = Vec::new();
for (vertex_id, required_edges) in &schema.required {
for edge in required_edges {
deps.push(EmbeddedDependency {
pattern_vertex: vertex_id.to_string(),
pattern_values: HashMap::new(),
consequence_vertex: edge.tgt.to_string(),
consequence_values: HashMap::new(),
});
}
}
deps
}
#[must_use]
pub fn dependencies_from_theory(theory: &Theory, schema: &Schema) -> Vec<EmbeddedDependency> {
let mut deps = Vec::new();
for eq in &theory.eqs {
deps.extend(translate_equation(eq, theory, schema));
}
deps
}
fn translate_equation(eq: &Equation, theory: &Theory, schema: &Schema) -> Vec<EmbeddedDependency> {
let mut deps = Vec::new();
let lhs_op = outermost_op(&eq.lhs);
let rhs_op = outermost_op(&eq.rhs);
match (lhs_op, rhs_op) {
(Some(lhs_name), Some(rhs_name)) => {
let lhs_sort = theory.find_op(&lhs_name).map(|op| op.output.to_string());
let rhs_sort = theory.find_op(&rhs_name).map(|op| op.output.to_string());
if let (Some(lhs_s), Some(rhs_s)) = (lhs_sort, rhs_sort) {
let lhs_vertex = find_vertex_by_kind(schema, &lhs_s);
let rhs_vertex = find_vertex_by_kind(schema, &rhs_s);
if let (Some(lv), Some(rv)) = (lhs_vertex, rhs_vertex) {
deps.push(EmbeddedDependency {
pattern_vertex: lv,
pattern_values: HashMap::new(),
consequence_vertex: rv,
consequence_values: HashMap::new(),
});
}
}
}
(Some(op_name), None) => {
if let Some(op) = theory.find_op(&op_name) {
let output_sort = op.output.head().to_string();
for (_, input_sort, _) in &op.inputs {
let out_vertex = find_vertex_by_kind(schema, &output_sort);
let in_vertex = find_vertex_by_kind(schema, input_sort.head());
if let (Some(ov), Some(iv)) = (out_vertex, in_vertex) {
deps.push(EmbeddedDependency {
pattern_vertex: ov,
pattern_values: HashMap::new(),
consequence_vertex: iv,
consequence_values: HashMap::new(),
});
}
}
}
}
(None, Some(op_name)) => {
if let Some(op) = theory.find_op(&op_name) {
let output_sort = op.output.head().to_string();
for (_, input_sort, _) in &op.inputs {
let out_vertex = find_vertex_by_kind(schema, &output_sort);
let in_vertex = find_vertex_by_kind(schema, input_sort.head());
if let (Some(ov), Some(iv)) = (out_vertex, in_vertex) {
deps.push(EmbeddedDependency {
pattern_vertex: iv,
pattern_values: HashMap::new(),
consequence_vertex: ov,
consequence_values: HashMap::new(),
});
}
}
}
}
(None, None) => {
}
}
deps
}
fn outermost_op(term: &Term) -> Option<String> {
match term {
Term::Var(_) | Term::Case { .. } | Term::Hole { .. } | Term::Let { .. } => None,
Term::App { op, .. } => Some(op.to_string()),
}
}
fn find_vertex_by_kind(schema: &Schema, sort_name: &str) -> Option<String> {
schema
.vertices
.values()
.find(|v| v.kind.as_str() == sort_name)
.map(|v| v.id.to_string())
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use panproto_gat::Name;
use panproto_schema::Schema;
use super::*;
fn row(col: &str, val: Value) -> HashMap<String, Value> {
HashMap::from([(col.to_owned(), val)])
}
#[test]
fn chase_no_change_when_constraints_satisfied() {
let instance = FInstance::new()
.with_table("A", vec![row("x", Value::Int(1))])
.with_table("B", vec![row("y", Value::Int(2))]);
let dep = EmbeddedDependency {
pattern_vertex: "A".to_owned(),
pattern_values: HashMap::from([("x".to_owned(), Value::Int(1))]),
consequence_vertex: "B".to_owned(),
consequence_values: HashMap::from([("y".to_owned(), Value::Int(2))]),
};
let result = chase_functor(&instance, &[dep], 10).unwrap();
assert_eq!(result.row_count("A"), 1);
assert_eq!(result.row_count("B"), 1);
}
#[test]
fn chase_adds_missing_consequence_row() {
let instance = FInstance::new().with_table("A", vec![row("x", Value::Int(1))]);
let dep = EmbeddedDependency {
pattern_vertex: "A".to_owned(),
pattern_values: HashMap::from([("x".to_owned(), Value::Int(1))]),
consequence_vertex: "B".to_owned(),
consequence_values: HashMap::from([("y".to_owned(), Value::Int(2))]),
};
let result = chase_functor(&instance, &[dep], 10).unwrap();
assert_eq!(result.row_count("A"), 1);
assert_eq!(result.row_count("B"), 1);
let b_rows = result.tables.get("B").unwrap();
assert_eq!(b_rows[0].get("y"), Some(&Value::Int(2)));
}
#[test]
fn chase_multi_iteration_fixpoint() {
let instance = FInstance::new().with_table("A", vec![row("x", Value::Int(1))]);
let deps = vec![
EmbeddedDependency {
pattern_vertex: "A".to_owned(),
pattern_values: HashMap::from([("x".to_owned(), Value::Int(1))]),
consequence_vertex: "B".to_owned(),
consequence_values: HashMap::from([("x".to_owned(), Value::Int(1))]),
},
EmbeddedDependency {
pattern_vertex: "B".to_owned(),
pattern_values: HashMap::from([("x".to_owned(), Value::Int(1))]),
consequence_vertex: "C".to_owned(),
consequence_values: HashMap::from([("x".to_owned(), Value::Int(1))]),
},
];
let result = chase_functor(&instance, &deps, 10).unwrap();
assert_eq!(result.row_count("A"), 1);
assert_eq!(result.row_count("B"), 1);
assert_eq!(result.row_count("C"), 1);
}
#[test]
fn chase_non_termination_error() {
let instance = FInstance::new().with_table("A", vec![row("x", Value::Int(1))]);
let dep = EmbeddedDependency {
pattern_vertex: "A".to_owned(),
pattern_values: HashMap::from([("x".to_owned(), Value::Int(1))]),
consequence_vertex: "B".to_owned(),
consequence_values: HashMap::from([("y".to_owned(), Value::Int(2))]),
};
let err = chase_functor(&instance, &[dep], 0).unwrap_err();
assert!(
matches!(err, ChaseError::NonTermination(0)),
"expected NonTermination(0), got {err:?}"
);
}
#[test]
fn chase_no_trigger_when_pattern_absent() {
let instance = FInstance::new().with_table("A", vec![row("x", Value::Int(99))]);
let dep = EmbeddedDependency {
pattern_vertex: "A".to_owned(),
pattern_values: HashMap::from([("x".to_owned(), Value::Int(1))]),
consequence_vertex: "B".to_owned(),
consequence_values: HashMap::from([("y".to_owned(), Value::Int(2))]),
};
let result = chase_functor(&instance, &[dep], 10).unwrap();
assert_eq!(result.row_count("B"), 0);
}
#[test]
fn dependencies_from_schema_empty_when_no_required() {
let schema = Schema {
protocol: "test".into(),
vertices: HashMap::new(),
edges: HashMap::new(),
hyper_edges: HashMap::new(),
constraints: HashMap::new(),
required: HashMap::new(),
nsids: HashMap::new(),
entries: Vec::new(),
variants: HashMap::new(),
orderings: HashMap::new(),
recursion_points: HashMap::new(),
spans: HashMap::new(),
usage_modes: HashMap::new(),
nominal: HashMap::new(),
coercions: HashMap::new(),
mergers: HashMap::new(),
defaults: HashMap::new(),
policies: HashMap::new(),
outgoing: HashMap::new(),
incoming: HashMap::new(),
between: HashMap::new(),
};
let deps = dependencies_from_schema(&schema);
assert!(deps.is_empty());
}
#[test]
fn dependencies_from_schema_extracts_required_edges() {
use panproto_schema::Edge;
let mut required = HashMap::new();
required.insert(
Name::from("user"),
vec![Edge {
src: Name::from("user"),
tgt: Name::from("profile"),
kind: Name::from("prop"),
name: Some(Name::from("profile")),
}],
);
let schema = Schema {
protocol: "test".into(),
vertices: HashMap::new(),
edges: HashMap::new(),
hyper_edges: HashMap::new(),
constraints: HashMap::new(),
required,
nsids: HashMap::new(),
entries: Vec::new(),
variants: HashMap::new(),
orderings: HashMap::new(),
recursion_points: HashMap::new(),
spans: HashMap::new(),
usage_modes: HashMap::new(),
nominal: HashMap::new(),
coercions: HashMap::new(),
mergers: HashMap::new(),
defaults: HashMap::new(),
policies: HashMap::new(),
outgoing: HashMap::new(),
incoming: HashMap::new(),
between: HashMap::new(),
};
let deps = dependencies_from_schema(&schema);
assert_eq!(deps.len(), 1);
assert_eq!(deps[0].pattern_vertex, "user");
assert_eq!(deps[0].consequence_vertex, "profile");
}
#[test]
fn dependencies_from_theory_retraction() {
use panproto_gat::{Equation, Operation, Sort, Term, Theory};
let theory = Theory::new(
"ThTest",
vec![Sort::simple("Vertex"), Sort::simple("Variant")],
vec![
Operation::unary("injection", "v", "Variant", "Vertex"),
Operation::unary("variant_of", "v", "Vertex", "Variant"),
],
vec![Equation::new(
"retraction",
Term::app(
"variant_of",
vec![Term::app("injection", vec![Term::var("v")])],
),
Term::var("v"),
)],
);
let schema = Schema {
protocol: "test".into(),
vertices: HashMap::from([
(
Name::from("v1"),
panproto_schema::Vertex {
id: Name::from("v1"),
kind: Name::from("Vertex"),
nsid: None,
},
),
(
Name::from("var1"),
panproto_schema::Vertex {
id: Name::from("var1"),
kind: Name::from("Variant"),
nsid: None,
},
),
]),
edges: HashMap::new(),
hyper_edges: HashMap::new(),
constraints: HashMap::new(),
required: HashMap::new(),
nsids: HashMap::new(),
entries: Vec::new(),
variants: HashMap::new(),
orderings: HashMap::new(),
recursion_points: HashMap::new(),
spans: HashMap::new(),
usage_modes: HashMap::new(),
nominal: HashMap::new(),
coercions: HashMap::new(),
mergers: HashMap::new(),
defaults: HashMap::new(),
policies: HashMap::new(),
outgoing: HashMap::new(),
incoming: HashMap::new(),
between: HashMap::new(),
};
let deps = dependencies_from_theory(&theory, &schema);
assert!(!deps.is_empty(), "retraction should produce dependencies");
}
#[test]
fn dependencies_from_theory_symmetric_graph() {
use panproto_gat::{Equation, Operation, Sort, Term, Theory};
let theory = Theory::new(
"ThSym",
vec![Sort::simple("Vertex"), Sort::simple("Edge")],
vec![
Operation::unary("inv", "e", "Edge", "Edge"),
Operation::unary("src", "e", "Edge", "Vertex"),
Operation::unary("tgt", "e", "Edge", "Vertex"),
],
vec![
Equation::new(
"src_inv",
Term::app("src", vec![Term::app("inv", vec![Term::var("e")])]),
Term::app("tgt", vec![Term::var("e")]),
),
Equation::new(
"inv_inv",
Term::app("inv", vec![Term::app("inv", vec![Term::var("e")])]),
Term::var("e"),
),
],
);
let schema = Schema {
protocol: "test".into(),
vertices: HashMap::from([
(
Name::from("v"),
panproto_schema::Vertex {
id: Name::from("v"),
kind: Name::from("Vertex"),
nsid: None,
},
),
(
Name::from("e"),
panproto_schema::Vertex {
id: Name::from("e"),
kind: Name::from("Edge"),
nsid: None,
},
),
]),
edges: HashMap::new(),
hyper_edges: HashMap::new(),
constraints: HashMap::new(),
required: HashMap::new(),
nsids: HashMap::new(),
entries: Vec::new(),
variants: HashMap::new(),
orderings: HashMap::new(),
recursion_points: HashMap::new(),
spans: HashMap::new(),
usage_modes: HashMap::new(),
nominal: HashMap::new(),
coercions: HashMap::new(),
mergers: HashMap::new(),
defaults: HashMap::new(),
policies: HashMap::new(),
outgoing: HashMap::new(),
incoming: HashMap::new(),
between: HashMap::new(),
};
let deps = dependencies_from_theory(&theory, &schema);
assert!(
deps.len() >= 2,
"symmetric graph equations should produce at least 2 dependencies, got {}",
deps.len()
);
}
#[test]
fn dependencies_from_theory_empty_equations() {
use panproto_gat::{Sort, Theory};
let theory = Theory::new(
"ThNoEqs",
vec![Sort::simple("Vertex")],
vec![],
vec![], );
let schema = Schema {
protocol: "test".into(),
vertices: HashMap::new(),
edges: HashMap::new(),
hyper_edges: HashMap::new(),
constraints: HashMap::new(),
required: HashMap::new(),
nsids: HashMap::new(),
entries: Vec::new(),
variants: HashMap::new(),
orderings: HashMap::new(),
recursion_points: HashMap::new(),
spans: HashMap::new(),
usage_modes: HashMap::new(),
nominal: HashMap::new(),
coercions: HashMap::new(),
mergers: HashMap::new(),
defaults: HashMap::new(),
policies: HashMap::new(),
outgoing: HashMap::new(),
incoming: HashMap::new(),
between: HashMap::new(),
};
let deps = dependencies_from_theory(&theory, &schema);
assert!(deps.is_empty(), "no equations means no dependencies");
}
}