pattern_core/lib.rs
1//! pattern-core - Core pattern data structures
2//!
3//! This crate provides the core pattern data structures for the pattern-rs library.
4//! It is a faithful port of the gram-hs reference implementation.
5//!
6//! # Overview
7//!
8//! The `pattern-core` crate defines two main types:
9//!
10//! - **[`Pattern<V>`](pattern::Pattern)**: A recursive, nested structure (s-expression-like)
11//! that is generic over value type `V`. This is the foundational data structure for
12//! representing nested, hierarchical data that may be interpreted as graphs.
13//!
14//! - **[`Subject`]**: A self-descriptive value type with identity, labels,
15//! and properties. Designed to be used as the value type in `Pattern<Subject>`, which is
16//! a common use case for replacing object-graphs with nested patterns.
17//!
18//! # Quick Start
19//!
20//! ```rust
21//! use pattern_core::{Pattern, Subject, Symbol, Value};
22//! use std::collections::{HashSet, HashMap};
23//!
24//! // Create an atomic pattern (special case)
25//! let atomic = Pattern::point("hello".to_string());
26//!
27//! // Create a pattern with elements (primary constructor)
28//! let pattern = Pattern::pattern("parent".to_string(), vec![
29//! Pattern::point("child1".to_string()),
30//! Pattern::point("child2".to_string()),
31//! ]);
32//!
33//! // Access pattern components
34//! assert_eq!(atomic.value(), "hello");
35//! assert_eq!(pattern.length(), 2);
36//! assert_eq!(pattern.depth(), 1);
37//!
38//! // Transform pattern values (Functor)
39//! let upper = pattern.clone().map(|s| s.to_uppercase());
40//! assert_eq!(upper.value(), "PARENT");
41//!
42//! // Validate pattern structure
43//! use pattern_core::ValidationRules;
44//! let rules = ValidationRules {
45//! max_depth: Some(10),
46//! ..Default::default()
47//! };
48//! assert!(pattern.validate(&rules).is_ok());
49//!
50//! // Analyze pattern structure
51//! let analysis = pattern.analyze_structure();
52//! println!("Structure: {}", analysis.summary);
53//!
54//! // Create a pattern with Subject value
55//! let subject = Subject {
56//! identity: Symbol("n".to_string()),
57//! labels: {
58//! let mut s = HashSet::new();
59//! s.insert("Person".to_string());
60//! s
61//! },
62//! properties: {
63//! let mut m = HashMap::new();
64//! m.insert("name".to_string(), Value::VString("Alice".to_string()));
65//! m
66//! },
67//! };
68//!
69//! let pattern_with_subject: Pattern<Subject> = Pattern::point(subject);
70//! ```
71//!
72//! # Pattern Combination
73//!
74//! Patterns can be combined associatively using the `combine()` method when the value type
75//! implements the `Combinable` trait. Combination merges two patterns by combining their values
76//! and concatenating their elements.
77//!
78//! ```rust
79//! use pattern_core::{Pattern, Combinable};
80//!
81//! // Combine atomic patterns (no elements)
82//! let p1 = Pattern::point("hello".to_string());
83//! let p2 = Pattern::point(" world".to_string());
84//! let combined = p1.combine(p2);
85//! assert_eq!(combined.value(), "hello world");
86//! assert_eq!(combined.length(), 0);
87//!
88//! // Combine patterns with elements
89//! let p3 = Pattern::pattern("a".to_string(), vec![
90//! Pattern::point("b".to_string()),
91//! Pattern::point("c".to_string()),
92//! ]);
93//! let p4 = Pattern::pattern("d".to_string(), vec![
94//! Pattern::point("e".to_string()),
95//! ]);
96//! let result = p3.combine(p4);
97//! assert_eq!(result.value(), "ad");
98//! assert_eq!(result.length(), 3); // [b, c, e]
99//!
100//! // Associativity: (a ⊕ b) ⊕ c = a ⊕ (b ⊕ c)
101//! let a = Pattern::point("a".to_string());
102//! let b = Pattern::point("b".to_string());
103//! let c = Pattern::point("c".to_string());
104//! let left = a.clone().combine(b.clone()).combine(c.clone());
105//! let right = a.combine(b.combine(c));
106//! assert_eq!(left, right);
107//! ```
108//!
109//! # Pattern Ordering
110//!
111//! Patterns implement `Ord` and `PartialOrd` for types that support ordering,
112//! enabling sorting, comparison, and use in ordered data structures.
113//!
114//! ```rust
115//! use pattern_core::Pattern;
116//! use std::collections::{BTreeSet, BTreeMap};
117//!
118//! // Compare patterns
119//! let p1 = Pattern::point(1);
120//! let p2 = Pattern::point(2);
121//! assert!(p1 < p2);
122//!
123//! // Value-first ordering: values compared before elements
124//! let p3 = Pattern::pattern(3, vec![Pattern::point(100)]);
125//! let p4 = Pattern::pattern(4, vec![Pattern::point(1)]);
126//! assert!(p3 < p4); // 3 < 4, elements not compared
127//!
128//! // Sort patterns
129//! let mut patterns = vec![
130//! Pattern::point(5),
131//! Pattern::point(2),
132//! Pattern::point(8),
133//! ];
134//! patterns.sort();
135//! assert_eq!(patterns[0], Pattern::point(2));
136//!
137//! // Find min/max
138//! let min = patterns.iter().min().unwrap();
139//! let max = patterns.iter().max().unwrap();
140//! assert_eq!(min, &Pattern::point(2));
141//! assert_eq!(max, &Pattern::point(8));
142//!
143//! // Use in BTreeSet (maintains sorted order)
144//! let mut set = BTreeSet::new();
145//! set.insert(Pattern::point(5));
146//! set.insert(Pattern::point(2));
147//! set.insert(Pattern::point(8));
148//! let sorted: Vec<_> = set.iter().map(|p| p.value).collect();
149//! assert_eq!(sorted, vec![2, 5, 8]);
150//!
151//! // Use as BTreeMap keys
152//! let mut map = BTreeMap::new();
153//! map.insert(Pattern::point(1), "first");
154//! map.insert(Pattern::point(2), "second");
155//! assert_eq!(map.get(&Pattern::point(1)), Some(&"first"));
156//! ```
157//!
158//! # WASM Compatibility
159//!
160//! All types in this crate are fully compatible with WebAssembly targets. Compile for WASM with:
161//!
162//! ```bash
163//! cargo build --package pattern-core --target wasm32-unknown-unknown
164//! ```
165//!
166//! # Reference Implementation
167//!
168//! This crate is ported from the gram-hs reference implementation:
169//! - Pattern: `../pattern-hs/libs/pattern/src/Pattern.hs`
170//! - Subject: `../pattern-hs/libs/subject/src/Subject/Core.hs`
171//! - Feature Spec: `../pattern-hs/specs/001-pattern-data-structure/`
172
173pub mod graph;
174pub mod pattern;
175pub mod pattern_graph;
176pub mod reconcile;
177pub mod subject;
178pub mod test_utils;
179
180#[cfg(feature = "python")]
181pub mod python;
182
183#[cfg(feature = "wasm")]
184pub mod wasm;
185
186pub use graph::{
187 all_paths, betweenness_centrality, bfs, canonical_classifier, classify_by_shape,
188 connected_components, degree_centrality, dfs, directed, directed_reverse, filter_graph,
189 fold_graph, frame_query, from_graph_lens, from_pattern_graph, from_test_node, has_cycle,
190 has_path, is_connected, is_neighbor, map_all_graph, map_graph, map_with_context, materialize,
191 memoize_incident_rels, minimum_spanning_tree, para_graph, para_graph_fixed,
192 query_annotations_of, query_co_members, query_walks_containing, shortest_path,
193 topological_sort, undirected, unfold_graph, CategoryMappers, GraphClass, GraphClassifier,
194 GraphQuery, GraphValue, GraphView, Substitution, TraversalDirection, TraversalWeight,
195};
196pub use pattern::{unfold, Pattern, StructureAnalysis, ValidationError, ValidationRules};
197pub use pattern_graph::{
198 from_pattern_graph as graph_query_from_pattern_graph, from_patterns, from_patterns_with_policy,
199 merge as pg_merge, merge_with_policy as pg_merge_with_policy, PatternGraph,
200};
201pub use reconcile::{
202 ElementMergeStrategy, HasIdentity, LabelMerge, Mergeable, PropertyMerge, ReconciliationPolicy,
203 Refinable, SubjectMergeStrategy,
204};
205pub use subject::{PropertyRecord, RangeValue, Subject, Symbol, Value};
206
207// Re-export comonad operations for convenient access
208// These are defined in pattern::comonad and pattern::comonad_helpers modules
209// All operations are methods on Pattern<V>, so no additional re-exports needed beyond Pattern itself
210
211// ============================================================================
212// Combinable Trait
213// ============================================================================
214
215/// Types that support associative combination.
216///
217/// Implementors must ensure that combination is associative:
218/// `(a.combine(b)).combine(c)` must equal `a.combine(b.combine(c))` for all values.
219///
220/// This trait is used to enable pattern combination for `Pattern<V>` where `V: Combinable`.
221///
222/// # Laws
223///
224/// **Associativity**: For all values a, b, c of type Self:
225/// ```text
226/// (a.combine(b)).combine(c) == a.combine(b.combine(c))
227/// ```
228///
229/// # Examples
230///
231/// ```rust
232/// use pattern_core::Combinable;
233///
234/// let s1 = String::from("hello");
235/// let s2 = String::from(" world");
236/// let result = s1.combine(s2);
237/// assert_eq!(result, "hello world");
238/// ```
239pub trait Combinable {
240 /// Combines two values associatively.
241 ///
242 /// # Parameters
243 ///
244 /// * `self` - The first value (consumed)
245 /// * `other` - The second value to combine with (consumed)
246 ///
247 /// # Returns
248 ///
249 /// A new value representing the combination of `self` and `other`.
250 ///
251 /// # Laws
252 ///
253 /// Must be associative: `(a.combine(b)).combine(c) == a.combine(b.combine(c))`
254 fn combine(self, other: Self) -> Self;
255}
256
257// ============================================================================
258// Standard Implementations
259// ============================================================================
260
261/// Combines two strings by concatenation.
262///
263/// String concatenation is associative: `(a + b) + c = a + (b + c)`
264///
265/// # Examples
266///
267/// ```rust
268/// use pattern_core::Combinable;
269///
270/// let s1 = String::from("hello");
271/// let s2 = String::from(" world");
272/// let result = s1.combine(s2);
273/// assert_eq!(result, "hello world");
274/// ```
275impl Combinable for String {
276 fn combine(mut self, other: Self) -> Self {
277 self.push_str(&other);
278 self
279 }
280}
281
282/// Combines two vectors by concatenation.
283///
284/// Vector concatenation is associative: `(a ++ b) ++ c = a ++ (b ++ c)`
285///
286/// # Examples
287///
288/// ```rust
289/// use pattern_core::Combinable;
290///
291/// let v1 = vec![1, 2, 3];
292/// let v2 = vec![4, 5];
293/// let result = v1.combine(v2);
294/// assert_eq!(result, vec![1, 2, 3, 4, 5]);
295/// ```
296impl<T> Combinable for Vec<T> {
297 fn combine(mut self, other: Self) -> Self {
298 self.extend(other);
299 self
300 }
301}
302
303/// Combines two unit values (trivial).
304///
305/// Unit combination is trivially associative.
306///
307/// # Examples
308///
309/// ```rust
310/// use pattern_core::Combinable;
311///
312/// let u1 = ();
313/// let u2 = ();
314/// let result = u1.combine(u2);
315/// assert_eq!(result, ());
316/// ```
317impl Combinable for () {
318 fn combine(self, _other: Self) -> Self {}
319}
320
321// ============================================================================
322// Subject Combination Strategies
323// ============================================================================
324
325/// Combination strategy for Subject that merges labels and properties.
326///
327/// This strategy combines two subjects by:
328/// - Using the identity from the first subject
329/// - Taking the union of labels from both subjects
330/// - Merging properties (values from the second subject overwrite the first)
331///
332/// # Semigroup Laws
333///
334/// This implementation satisfies associativity:
335/// - Identity choice is associative (always picks leftmost)
336/// - Label union is associative (set union is associative)
337/// - Property merge is associative with right-bias (latter values win)
338///
339/// # Examples
340///
341/// ```rust
342/// use pattern_core::{Subject, Symbol, Combinable};
343/// use std::collections::{HashMap, HashSet};
344///
345/// let s1 = Subject {
346/// identity: Symbol("n1".to_string()),
347/// labels: {
348/// let mut s = HashSet::new();
349/// s.insert("Person".to_string());
350/// s
351/// },
352/// properties: HashMap::new(),
353/// };
354///
355/// let s2 = Subject {
356/// identity: Symbol("n2".to_string()),
357/// labels: {
358/// let mut s = HashSet::new();
359/// s.insert("Employee".to_string());
360/// s
361/// },
362/// properties: HashMap::new(),
363/// };
364///
365/// // Merge combines labels and uses first identity
366/// let merged = s1.combine(s2);
367/// assert_eq!(merged.identity.0, "n1");
368/// assert!(merged.labels.contains("Person"));
369/// assert!(merged.labels.contains("Employee"));
370/// ```
371impl Combinable for Subject {
372 fn combine(self, other: Self) -> Self {
373 // Keep first identity (leftmost in associative chain)
374 let identity = self.identity;
375
376 // Union of labels (set union is associative)
377 let labels = self.labels.union(&other.labels).cloned().collect();
378
379 // Merge properties (right overwrites left)
380 let mut properties = self.properties;
381 properties.extend(other.properties);
382
383 Subject {
384 identity,
385 labels,
386 properties,
387 }
388 }
389}
390
391/// Newtype wrapper for "first wins" combination strategy.
392///
393/// When combining two FirstSubject instances, the first subject is returned
394/// and the second is discarded. This is useful for scenarios where you want
395/// to keep the initial subject and ignore subsequent ones.
396///
397/// # Semigroup Laws
398///
399/// This satisfies associativity trivially: first(first(a, b), c) = first(a, first(b, c)) = a
400///
401/// # Examples
402///
403/// ```rust
404/// use pattern_core::{Subject, Symbol, Combinable};
405/// use std::collections::HashSet;
406///
407/// let s1 = Subject {
408/// identity: Symbol("alice".to_string()),
409/// labels: HashSet::new(),
410/// properties: Default::default(),
411/// };
412///
413/// let s2 = Subject {
414/// identity: Symbol("bob".to_string()),
415/// labels: HashSet::new(),
416/// properties: Default::default(),
417/// };
418///
419/// // First wins - s2 is discarded
420/// let result = s1.combine(s2);
421/// assert_eq!(result.identity.0, "alice");
422/// ```
423#[derive(Clone, PartialEq)]
424pub struct FirstSubject(pub Subject);
425
426impl Combinable for FirstSubject {
427 fn combine(self, _other: Self) -> Self {
428 self // Always return first, discard second
429 }
430}
431
432/// Newtype wrapper for "last wins" combination strategy.
433///
434/// When combining two LastSubject instances, the second subject is returned
435/// and the first is discarded. This is useful for scenarios where you want
436/// the most recent subject to take precedence.
437///
438/// # Semigroup Laws
439///
440/// This satisfies associativity trivially: last(last(a, b), c) = last(a, last(b, c)) = c
441///
442/// # Examples
443///
444/// ```rust
445/// use pattern_core::{Subject, Symbol, Combinable, LastSubject};
446/// use std::collections::HashSet;
447///
448/// let s1 = LastSubject(Subject {
449/// identity: Symbol("alice".to_string()),
450/// labels: HashSet::new(),
451/// properties: Default::default(),
452/// });
453///
454/// let s2 = LastSubject(Subject {
455/// identity: Symbol("bob".to_string()),
456/// labels: HashSet::new(),
457/// properties: Default::default(),
458/// });
459///
460/// // Last wins - s1 is the last argument, so it wins
461/// let result = s2.combine(s1);
462/// assert_eq!(result.0.identity.0, "alice");
463/// ```
464#[derive(Clone, PartialEq)]
465pub struct LastSubject(pub Subject);
466
467impl Combinable for LastSubject {
468 fn combine(self, other: Self) -> Self {
469 other // Always return second, discard first
470 }
471}
472
473/// Newtype wrapper for "empty" combination strategy that creates anonymous subjects.
474///
475/// When combining two EmptySubject instances, the result is always an anonymous
476/// subject with no labels or properties. This serves as the identity element for
477/// a Monoid-like structure.
478///
479/// # Semigroup Laws
480///
481/// This satisfies associativity trivially: empty(empty(a, b), c) = empty(a, empty(b, c)) = empty
482///
483/// # Monoid Laws
484///
485/// When used with Default, this provides monoid identity:
486/// - Left identity: empty.combine(s) = empty
487/// - Right identity: s.combine(empty) = empty
488///
489/// # Examples
490///
491/// ```rust
492/// use pattern_core::{Subject, Symbol, Combinable, EmptySubject};
493/// use std::collections::HashSet;
494///
495/// let s1 = EmptySubject(Subject {
496/// identity: Symbol("alice".to_string()),
497/// labels: {
498/// let mut s = HashSet::new();
499/// s.insert("Person".to_string());
500/// s
501/// },
502/// properties: Default::default(),
503/// });
504///
505/// let empty = EmptySubject(Subject {
506/// identity: Symbol("_".to_string()),
507/// labels: HashSet::new(),
508/// properties: Default::default(),
509/// });
510///
511/// // Always returns empty (anonymous)
512/// let result = s1.combine(empty);
513/// assert_eq!(result.0.identity.0, "_");
514/// assert!(result.0.labels.is_empty());
515/// ```
516#[derive(Clone, PartialEq)]
517pub struct EmptySubject(pub Subject);
518
519impl Combinable for EmptySubject {
520 fn combine(self, _other: Self) -> Self {
521 // Always return anonymous empty subject
522 EmptySubject(Subject {
523 identity: Symbol("_".to_string()),
524 labels: Default::default(),
525 properties: Default::default(),
526 })
527 }
528}
529
530impl Default for EmptySubject {
531 fn default() -> Self {
532 EmptySubject(Subject {
533 identity: Symbol("_".to_string()),
534 labels: Default::default(),
535 properties: Default::default(),
536 })
537 }
538}
539
540#[cfg(test)]
541mod tests {
542 use super::*;
543 use std::collections::{HashMap, HashSet};
544
545 #[test]
546 fn subject_merge_combines_labels_and_properties() {
547 let s1 = Subject {
548 identity: Symbol("n1".to_string()),
549 labels: {
550 let mut s = HashSet::new();
551 s.insert("Person".to_string());
552 s
553 },
554 properties: {
555 let mut m = HashMap::new();
556 m.insert("name".to_string(), Value::VString("Alice".to_string()));
557 m
558 },
559 };
560
561 let s2 = Subject {
562 identity: Symbol("n2".to_string()),
563 labels: {
564 let mut s = HashSet::new();
565 s.insert("Employee".to_string());
566 s
567 },
568 properties: {
569 let mut m = HashMap::new();
570 m.insert("role".to_string(), Value::VString("Engineer".to_string()));
571 m
572 },
573 };
574
575 let merged = s1.combine(s2);
576
577 assert_eq!(merged.identity.0, "n1");
578 assert_eq!(merged.labels.len(), 2);
579 assert!(merged.labels.contains("Person"));
580 assert!(merged.labels.contains("Employee"));
581 assert_eq!(merged.properties.len(), 2);
582 }
583
584 #[test]
585 fn subject_merge_is_associative() {
586 let s1 = Subject {
587 identity: Symbol("a".to_string()),
588 labels: {
589 let mut s = HashSet::new();
590 s.insert("L1".to_string());
591 s
592 },
593 properties: HashMap::new(),
594 };
595
596 let s2 = Subject {
597 identity: Symbol("b".to_string()),
598 labels: {
599 let mut s = HashSet::new();
600 s.insert("L2".to_string());
601 s
602 },
603 properties: HashMap::new(),
604 };
605
606 let s3 = Subject {
607 identity: Symbol("c".to_string()),
608 labels: {
609 let mut s = HashSet::new();
610 s.insert("L3".to_string());
611 s
612 },
613 properties: HashMap::new(),
614 };
615
616 // (s1 + s2) + s3
617 let left = s1.clone().combine(s2.clone()).combine(s3.clone());
618
619 // s1 + (s2 + s3)
620 let right = s1.combine(s2.combine(s3));
621
622 assert_eq!(left.identity, right.identity);
623 assert_eq!(left.labels, right.labels);
624 }
625
626 #[test]
627 fn first_subject_keeps_first() {
628 let s1 = FirstSubject(Subject {
629 identity: Symbol("alice".to_string()),
630 labels: HashSet::new(),
631 properties: HashMap::new(),
632 });
633
634 let s2 = FirstSubject(Subject {
635 identity: Symbol("bob".to_string()),
636 labels: HashSet::new(),
637 properties: HashMap::new(),
638 });
639
640 let result = s1.clone().combine(s2);
641 assert_eq!(result.0.identity.0, "alice");
642 }
643
644 #[test]
645 fn last_subject_keeps_last() {
646 let s1 = LastSubject(Subject {
647 identity: Symbol("alice".to_string()),
648 labels: HashSet::new(),
649 properties: HashMap::new(),
650 });
651
652 let s2 = LastSubject(Subject {
653 identity: Symbol("bob".to_string()),
654 labels: HashSet::new(),
655 properties: HashMap::new(),
656 });
657
658 let result = s1.combine(s2.clone());
659 assert_eq!(result.0.identity.0, "bob");
660 }
661
662 #[test]
663 fn empty_subject_returns_anonymous() {
664 let s1 = EmptySubject(Subject {
665 identity: Symbol("alice".to_string()),
666 labels: {
667 let mut s = HashSet::new();
668 s.insert("Person".to_string());
669 s
670 },
671 properties: HashMap::new(),
672 });
673
674 let s2 = EmptySubject(Subject {
675 identity: Symbol("bob".to_string()),
676 labels: HashSet::new(),
677 properties: HashMap::new(),
678 });
679
680 let result = s1.combine(s2);
681 assert_eq!(result.0.identity.0, "_");
682 assert!(result.0.labels.is_empty());
683 assert!(result.0.properties.is_empty());
684 }
685
686 #[test]
687 fn empty_subject_is_identity() {
688 let empty = EmptySubject::default();
689 let result = empty.clone().combine(empty);
690 assert_eq!(result.0.identity.0, "_");
691 }
692}