use crate::interval::Interval;
use crate::pattern::Target;
use std::collections::HashMap;
use std::fmt::Debug;
use std::hash::{Hash, Hasher};
pub(super) type MatchCandidate<N, V, T> = (
HashMap<String, BoundValue<N, V>>,
HashMap<String, Interval<T>>,
);
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Match<N: Debug + PartialEq, V: Debug + PartialEq, T: Debug + Clone + PartialEq> {
pub pattern: String,
pub pattern_idx: Option<usize>,
pub bindings: HashMap<String, BoundValue<N, V>>,
pub intervals: HashMap<String, Interval<T>>,
pub metadata: HashMap<String, String>,
}
fn hash_map_order_independent<K: Hash, V: Hash, H: Hasher>(map: &HashMap<K, V>, state: &mut H) {
let mut xor: u64 = 0;
for (k, v) in map {
let mut entry_hasher = std::collections::hash_map::DefaultHasher::new();
k.hash(&mut entry_hasher);
v.hash(&mut entry_hasher);
xor ^= std::hash::Hasher::finish(&entry_hasher);
}
xor.hash(state);
map.len().hash(state);
}
impl<N, V, T> Hash for Match<N, V, T>
where
N: Debug + PartialEq + Hash,
V: Debug + PartialEq + Hash,
T: Debug + Clone + PartialEq + Hash,
{
fn hash<H: Hasher>(&self, state: &mut H) {
self.pattern.hash(state);
self.pattern_idx.hash(state);
hash_map_order_independent(&self.bindings, state);
hash_map_order_independent(&self.intervals, state);
hash_map_order_independent(&self.metadata, state);
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum BoundValue<N: Debug, V: Debug> {
Node(N),
Value(V),
}
impl<N: Debug + Hash, V: Debug + Hash> Hash for BoundValue<N, V> {
fn hash<H: Hasher>(&self, state: &mut H) {
std::mem::discriminant(self).hash(state);
match self {
BoundValue::Node(n) => n.hash(state),
BoundValue::Value(v) => v.hash(state),
}
}
}
impl<N: Debug + PartialEq, V: Debug + PartialEq> BoundValue<N, V> {
pub(crate) fn matches_value(
&self,
value_as_node: &impl Fn(&V) -> Option<N>,
value: &V,
) -> bool {
match self {
BoundValue::Node(n) => {
value_as_node(value).is_some_and(|vn| &vn == n)
}
BoundValue::Value(v) => value == v,
}
}
}
#[derive(Debug, Clone)]
pub struct PartialMatch<N: Debug + Clone, V: Debug + Clone, T: Clone> {
pub pattern_idx: usize,
pub bindings: HashMap<String, BoundValue<N, V>>,
pub intervals: HashMap<String, Interval<T>>,
pub next_stage: usize,
pub state: MatchState,
pub id: usize,
pub created_at: T,
pub fingerprint: u64,
pub created_at_tick: u64,
pub repetition_count: u32,
pub matched_stages: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MatchState {
Active,
Complete,
Dead,
}
#[derive(Debug)]
pub enum SiftEvent<N: Debug, V: Debug> {
Advanced {
pattern: String,
match_id: usize,
stage_index: usize,
metadata: HashMap<String, String>,
},
Completed {
pattern: String,
match_id: usize,
bindings: HashMap<String, BoundValue<N, V>>,
metadata: HashMap<String, String>,
},
Negated {
pattern: String,
match_id: usize,
clause_label: String,
trigger_source: N,
metadata: HashMap<String, String>,
},
Expired {
pattern: String,
match_id: usize,
bindings: HashMap<String, BoundValue<N, V>>,
stage_reached: usize,
ticks_elapsed: u64,
metadata: HashMap<String, String>,
},
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GapAnalysis<L, V> {
pub pattern: String,
pub stages: Vec<StageAnalysis<L, V>>,
}
impl<L, V> GapAnalysis<L, V> {
pub fn closeness(&self) -> f64 {
let mut total = 0usize;
let mut matched = 0usize;
for stage in &self.stages {
for clause in &stage.clauses {
total += 1;
if clause.matched {
matched += 1;
}
}
}
if total == 0 {
0.0
} else {
matched as f64 / total as f64
}
}
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct StageAnalysis<L, V> {
pub anchor: String,
pub status: StageStatus,
pub clauses: Vec<ClauseAnalysis<L, V>>,
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum StageStatus {
Matched,
PartiallyMatched { matched: usize, total: usize },
Unmatched,
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ClauseAnalysis<L, V> {
pub description: String,
pub matched: bool,
pub reason: Option<String>,
pub source_var: String,
pub label: L,
pub target: Target<V>,
pub negated: bool,
}
#[derive(Debug, Clone, Default)]
pub struct EngineStats {
pub total_on_edge_added: u64,
pub total_fingerprints: u64,
pub total_negation_checks: u64,
pub peak_active_pms: usize,
}
#[derive(Debug, Clone, Default)]
pub struct PatternMetrics {
pub enabled: bool,
pub last_advanced_tick: u64,
pub completion_count: u64,
pub advancement_count: u64,
pub negation_count: u64,
pub active_pm_count: usize,
}
#[derive(Debug, Clone, Default)]
pub struct TickDelta {
pub advanced: Vec<String>,
pub completed: Vec<String>,
pub negated: Vec<String>,
pub expired: Vec<String>,
pub stalled: Vec<String>,
pub active_pm_count: usize,
}
#[derive(Debug, Clone)]
pub struct PlantPayoffPair {
pub plant_idx: usize,
pub payoff_idx: usize,
pub shared_binding: Option<String>,
}
#[derive(Debug, Clone)]
pub struct PlantStatus {
pub plant_pattern: String,
pub payoff_pattern: String,
pub active_plants: usize,
pub payoff_completions: u64,
pub ticks_since_plant_advanced: u64,
pub stale: bool,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pattern::Var;
fn clause(desc: &str, matched: bool, reason: Option<&str>) -> ClauseAnalysis<String, String> {
ClauseAnalysis {
description: desc.into(),
matched,
reason: reason.map(|s| s.into()),
source_var: "x".into(),
label: "test".into(),
target: Target::Bind(Var::new("y")),
negated: false,
}
}
#[test]
fn closeness_empty_analysis() {
let gap: GapAnalysis<String, String> = GapAnalysis {
pattern: "test".to_string(),
stages: vec![],
};
assert_eq!(gap.closeness(), 0.0);
}
#[test]
fn closeness_all_matched() {
let gap = GapAnalysis {
pattern: "test".to_string(),
stages: vec![StageAnalysis {
anchor: "e1".to_string(),
status: StageStatus::Matched,
clauses: vec![clause("a", true, None), clause("b", true, None)],
}],
};
assert_eq!(gap.closeness(), 1.0);
}
#[test]
fn closeness_partial() {
let gap = GapAnalysis {
pattern: "test".to_string(),
stages: vec![
StageAnalysis {
anchor: "e1".to_string(),
status: StageStatus::Matched,
clauses: vec![clause("a", true, None), clause("b", true, None)],
},
StageAnalysis {
anchor: "e2".to_string(),
status: StageStatus::PartiallyMatched {
matched: 1,
total: 2,
},
clauses: vec![
clause("c", true, None),
clause("d", false, Some("no match")),
],
},
],
};
assert_eq!(gap.closeness(), 0.75); }
#[test]
fn closeness_none_matched() {
let gap = GapAnalysis {
pattern: "test".to_string(),
stages: vec![StageAnalysis {
anchor: "e1".to_string(),
status: StageStatus::Unmatched,
clauses: vec![clause("a", false, None)],
}],
};
assert_eq!(gap.closeness(), 0.0);
}
}