Skip to main content

tensorlogic_adapters/
computed.rs

1//! Computed and virtual domains for derived types.
2//!
3//! Computed domains represent types that are derived from other domains
4//! through operations or transformations. They enable lazy evaluation
5//! and dynamic domain generation.
6//!
7//! # Examples
8//!
9//! ```rust
10//! use tensorlogic_adapters::{ComputedDomain, DomainComputation};
11//!
12//! // Create a filtered domain
13//! let adults = ComputedDomain::new(
14//!     "Adults",
15//!     DomainComputation::Filter {
16//!         base: "Person".to_string(),
17//!         predicate: "is_adult".to_string(),
18//!     }
19//! );
20//! ```
21
22use serde::{Deserialize, Serialize};
23use std::collections::HashMap;
24use std::fmt;
25
26use crate::{AdapterError, SymbolTable};
27
28/// Types of domain computations.
29///
30/// Each variant represents a different way of deriving a new domain
31/// from existing domains.
32#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
33pub enum DomainComputation {
34    /// Filter a base domain by a predicate.
35    ///
36    /// Creates a subset of the base domain containing only elements
37    /// satisfying the predicate.
38    Filter {
39        /// Base domain to filter.
40        base: String,
41        /// Predicate name for filtering.
42        predicate: String,
43    },
44
45    /// Union of multiple domains.
46    ///
47    /// Creates a domain containing elements from all source domains.
48    Union {
49        /// Source domains to union.
50        domains: Vec<String>,
51    },
52
53    /// Intersection of multiple domains.
54    ///
55    /// Creates a domain containing only elements present in all source domains.
56    Intersection {
57        /// Source domains to intersect.
58        domains: Vec<String>,
59    },
60
61    /// Difference between two domains (A - B).
62    ///
63    /// Creates a domain containing elements in the first domain but not in the second.
64    Difference {
65        /// Base domain.
66        base: String,
67        /// Domain to subtract.
68        subtract: String,
69    },
70
71    /// Product of domains.
72    ///
73    /// Creates a cartesian product of the source domains.
74    Product {
75        /// Domains to take product of.
76        domains: Vec<String>,
77    },
78
79    /// Power set of a domain.
80    ///
81    /// Creates a domain containing all subsets of the base domain.
82    PowerSet {
83        /// Base domain.
84        base: String,
85    },
86
87    /// Projection from a product domain.
88    ///
89    /// Extracts a component from a product domain.
90    Projection {
91        /// Product domain to project from.
92        product: String,
93        /// Index of component to project.
94        index: usize,
95    },
96
97    /// Custom computation with a formula.
98    ///
99    /// Allows user-defined domain computations with arbitrary logic.
100    Custom {
101        /// Description of the computation.
102        description: String,
103        /// Formula or implementation reference.
104        formula: String,
105    },
106}
107
108impl fmt::Display for DomainComputation {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        match self {
111            Self::Filter { base, predicate } => write!(f, "{{ x ∈ {} | {}(x) }}", base, predicate),
112            Self::Union { domains } => write!(f, "{}", domains.join(" ∪ ")),
113            Self::Intersection { domains } => write!(f, "{}", domains.join(" ∩ ")),
114            Self::Difference { base, subtract } => write!(f, "{} \\ {}", base, subtract),
115            Self::Product { domains } => write!(f, "{}", domains.join(" × ")),
116            Self::PowerSet { base } => write!(f, "℘({})", base),
117            Self::Projection { product, index } => write!(f, "π{}({})", index, product),
118            Self::Custom { description, .. } => write!(f, "{}", description),
119        }
120    }
121}
122
123/// A computed domain that is derived from other domains.
124///
125/// Computed domains are evaluated lazily and can represent complex
126/// domain transformations without materializing all elements.
127///
128/// # Examples
129///
130/// ```rust
131/// use tensorlogic_adapters::{ComputedDomain, DomainComputation};
132///
133/// // Filter domain
134/// let adults = ComputedDomain::new(
135///     "Adults",
136///     DomainComputation::Filter {
137///         base: "Person".to_string(),
138///         predicate: "is_adult".to_string(),
139///     }
140/// );
141///
142/// // Union domain
143/// let entities = ComputedDomain::new(
144///     "Entities",
145///     DomainComputation::Union {
146///         domains: vec!["Person".to_string(), "Organization".to_string()],
147///     }
148/// );
149/// ```
150#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
151pub struct ComputedDomain {
152    /// Name of the computed domain.
153    name: String,
154    /// Computation defining this domain.
155    computation: DomainComputation,
156    /// Optional cardinality estimate or bound.
157    cardinality_estimate: Option<usize>,
158    /// Whether this domain is materialized (computed and cached).
159    materialized: bool,
160}
161
162impl ComputedDomain {
163    /// Create a new computed domain.
164    ///
165    /// # Examples
166    ///
167    /// ```rust
168    /// use tensorlogic_adapters::{ComputedDomain, DomainComputation};
169    ///
170    /// let domain = ComputedDomain::new(
171    ///     "FilteredDomain",
172    ///     DomainComputation::Filter {
173    ///         base: "Base".to_string(),
174    ///         predicate: "pred".to_string(),
175    ///     }
176    /// );
177    /// ```
178    pub fn new(name: impl Into<String>, computation: DomainComputation) -> Self {
179        Self {
180            name: name.into(),
181            computation,
182            cardinality_estimate: None,
183            materialized: false,
184        }
185    }
186
187    /// Set a cardinality estimate for this computed domain.
188    ///
189    /// # Examples
190    ///
191    /// ```rust
192    /// use tensorlogic_adapters::{ComputedDomain, DomainComputation};
193    ///
194    /// let domain = ComputedDomain::new(
195    ///     "Adults",
196    ///     DomainComputation::Filter {
197    ///         base: "Person".to_string(),
198    ///         predicate: "is_adult".to_string(),
199    ///     }
200    /// ).with_cardinality_estimate(750);
201    /// ```
202    pub fn with_cardinality_estimate(mut self, estimate: usize) -> Self {
203        self.cardinality_estimate = Some(estimate);
204        self
205    }
206
207    /// Get the name of this computed domain.
208    pub fn name(&self) -> &str {
209        &self.name
210    }
211
212    /// Get the computation defining this domain.
213    pub fn computation(&self) -> &DomainComputation {
214        &self.computation
215    }
216
217    /// Get the cardinality estimate if available.
218    pub fn cardinality_estimate(&self) -> Option<usize> {
219        self.cardinality_estimate
220    }
221
222    /// Check if this domain is materialized.
223    pub fn is_materialized(&self) -> bool {
224        self.materialized
225    }
226
227    /// Compute the cardinality bounds for this domain.
228    ///
229    /// Returns (lower_bound, upper_bound) or an error if dependencies are missing.
230    pub fn cardinality_bounds(&self, table: &SymbolTable) -> Result<(usize, usize), AdapterError> {
231        match &self.computation {
232            DomainComputation::Filter { base, .. } => {
233                let base_card = table
234                    .get_domain(base)
235                    .ok_or_else(|| AdapterError::UnknownDomain(base.clone()))?
236                    .cardinality;
237                // Lower bound is 0 (could filter all), upper bound is base cardinality
238                Ok((0, base_card))
239            }
240            DomainComputation::Union { domains } => {
241                let max_card = domains
242                    .iter()
243                    .map(|d| {
244                        table
245                            .get_domain(d)
246                            .map(|info| info.cardinality)
247                            .ok_or_else(|| AdapterError::UnknownDomain(d.clone()))
248                    })
249                    .collect::<Result<Vec<_>, _>>()?
250                    .into_iter()
251                    .max()
252                    .unwrap_or(0);
253                let sum_card: usize = domains
254                    .iter()
255                    .map(|d| {
256                        table
257                            .get_domain(d)
258                            .expect("domain reference is valid")
259                            .cardinality
260                    })
261                    .sum();
262                Ok((max_card, sum_card))
263            }
264            DomainComputation::Intersection { domains } => {
265                let min_card = domains
266                    .iter()
267                    .map(|d| {
268                        table
269                            .get_domain(d)
270                            .map(|info| info.cardinality)
271                            .ok_or_else(|| AdapterError::UnknownDomain(d.clone()))
272                    })
273                    .collect::<Result<Vec<_>, _>>()?
274                    .into_iter()
275                    .min()
276                    .unwrap_or(0);
277                Ok((0, min_card))
278            }
279            DomainComputation::Difference { base, subtract } => {
280                let base_card = table
281                    .get_domain(base)
282                    .ok_or_else(|| AdapterError::UnknownDomain(base.clone()))?
283                    .cardinality;
284                let sub_card = table
285                    .get_domain(subtract)
286                    .ok_or_else(|| AdapterError::UnknownDomain(subtract.clone()))?
287                    .cardinality;
288                Ok((0, base_card.saturating_sub(sub_card)))
289            }
290            DomainComputation::Product { domains } => {
291                let product: usize = domains
292                    .iter()
293                    .map(|d| {
294                        table
295                            .get_domain(d)
296                            .map(|info| info.cardinality)
297                            .ok_or_else(|| AdapterError::UnknownDomain(d.clone()))
298                    })
299                    .collect::<Result<Vec<_>, _>>()?
300                    .into_iter()
301                    .product();
302                Ok((product, product))
303            }
304            DomainComputation::PowerSet { base } => {
305                let base_card = table
306                    .get_domain(base)
307                    .ok_or_else(|| AdapterError::UnknownDomain(base.clone()))?
308                    .cardinality;
309                let power = 2_usize.pow(base_card as u32);
310                Ok((power, power))
311            }
312            DomainComputation::Projection { product, index: _ } => {
313                let prod_domain = table
314                    .get_domain(product)
315                    .ok_or_else(|| AdapterError::UnknownDomain(product.clone()))?;
316                // This is simplified - in real implementation we'd parse product structure
317                Ok((0, prod_domain.cardinality))
318            }
319            DomainComputation::Custom { .. } => {
320                // For custom computations, use estimate if available
321                if let Some(est) = self.cardinality_estimate {
322                    Ok((0, est))
323                } else {
324                    Err(AdapterError::InvalidOperation(
325                        "Custom domain computation requires cardinality estimate".to_string(),
326                    ))
327                }
328            }
329        }
330    }
331
332    /// Validate that all dependencies exist in the symbol table.
333    pub fn validate(&self, table: &SymbolTable) -> Result<(), AdapterError> {
334        match &self.computation {
335            DomainComputation::Filter { base, predicate } => {
336                if table.get_domain(base).is_none() {
337                    return Err(AdapterError::UnknownDomain(base.clone()));
338                }
339                if table.get_predicate(predicate).is_none() {
340                    return Err(AdapterError::UnknownPredicate(predicate.clone()));
341                }
342            }
343            DomainComputation::Union { domains } | DomainComputation::Intersection { domains } => {
344                for domain in domains {
345                    if table.get_domain(domain).is_none() {
346                        return Err(AdapterError::UnknownDomain(domain.clone()));
347                    }
348                }
349            }
350            DomainComputation::Difference { base, subtract } => {
351                if table.get_domain(base).is_none() {
352                    return Err(AdapterError::UnknownDomain(base.clone()));
353                }
354                if table.get_domain(subtract).is_none() {
355                    return Err(AdapterError::UnknownDomain(subtract.clone()));
356                }
357            }
358            DomainComputation::Product { domains } => {
359                for domain in domains {
360                    if table.get_domain(domain).is_none() {
361                        return Err(AdapterError::UnknownDomain(domain.clone()));
362                    }
363                }
364            }
365            DomainComputation::PowerSet { base } => {
366                if table.get_domain(base).is_none() {
367                    return Err(AdapterError::UnknownDomain(base.clone()));
368                }
369            }
370            DomainComputation::Projection { product, .. } => {
371                if table.get_domain(product).is_none() {
372                    return Err(AdapterError::UnknownDomain(product.clone()));
373                }
374            }
375            DomainComputation::Custom { .. } => {
376                // Custom computations can't be validated automatically
377            }
378        }
379        Ok(())
380    }
381
382    /// Mark this domain as materialized.
383    pub fn materialize(&mut self) {
384        self.materialized = true;
385    }
386}
387
388impl fmt::Display for ComputedDomain {
389    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390        write!(f, "{} := {}", self.name, self.computation)
391    }
392}
393
394/// Registry for managing computed domains.
395///
396/// The registry tracks computed domains and their dependencies,
397/// enabling lazy evaluation and caching.
398#[derive(Clone, Debug, Default)]
399pub struct ComputedDomainRegistry {
400    /// Registered computed domains.
401    domains: HashMap<String, ComputedDomain>,
402}
403
404impl ComputedDomainRegistry {
405    /// Create a new empty registry.
406    pub fn new() -> Self {
407        Self {
408            domains: HashMap::new(),
409        }
410    }
411
412    /// Register a computed domain.
413    ///
414    /// # Examples
415    ///
416    /// ```rust
417    /// use tensorlogic_adapters::{ComputedDomainRegistry, ComputedDomain, DomainComputation};
418    ///
419    /// let mut registry = ComputedDomainRegistry::new();
420    /// let domain = ComputedDomain::new(
421    ///     "Adults",
422    ///     DomainComputation::Filter {
423    ///         base: "Person".to_string(),
424    ///         predicate: "is_adult".to_string(),
425    ///     }
426    /// );
427    /// registry.register(domain).expect("unwrap");
428    /// ```
429    pub fn register(&mut self, domain: ComputedDomain) -> Result<(), AdapterError> {
430        let name = domain.name().to_string();
431        if self.domains.contains_key(&name) {
432            return Err(AdapterError::DuplicateDomain(name));
433        }
434        self.domains.insert(name, domain);
435        Ok(())
436    }
437
438    /// Get a computed domain by name.
439    pub fn get(&self, name: &str) -> Option<&ComputedDomain> {
440        self.domains.get(name)
441    }
442
443    /// Get a mutable reference to a computed domain.
444    pub fn get_mut(&mut self, name: &str) -> Option<&mut ComputedDomain> {
445        self.domains.get_mut(name)
446    }
447
448    /// List all registered computed domains.
449    pub fn list(&self) -> Vec<&ComputedDomain> {
450        self.domains.values().collect()
451    }
452
453    /// Remove a computed domain from the registry.
454    pub fn remove(&mut self, name: &str) -> Option<ComputedDomain> {
455        self.domains.remove(name)
456    }
457
458    /// Validate all computed domains against a symbol table.
459    pub fn validate_all(&self, table: &SymbolTable) -> Result<(), Vec<AdapterError>> {
460        let errors: Vec<_> = self
461            .domains
462            .values()
463            .filter_map(|domain| domain.validate(table).err())
464            .collect();
465
466        if errors.is_empty() {
467            Ok(())
468        } else {
469            Err(errors)
470        }
471    }
472
473    /// Get the number of registered domains.
474    pub fn len(&self) -> usize {
475        self.domains.len()
476    }
477
478    /// Check if the registry is empty.
479    pub fn is_empty(&self) -> bool {
480        self.domains.is_empty()
481    }
482}
483
484#[cfg(test)]
485mod tests {
486    use super::*;
487    use crate::{DomainInfo, PredicateInfo};
488
489    #[test]
490    fn test_filter_computation() {
491        let comp = DomainComputation::Filter {
492            base: "Person".to_string(),
493            predicate: "is_adult".to_string(),
494        };
495        assert!(comp.to_string().contains("Person"));
496        assert!(comp.to_string().contains("is_adult"));
497    }
498
499    #[test]
500    fn test_union_computation() {
501        let comp = DomainComputation::Union {
502            domains: vec!["A".to_string(), "B".to_string(), "C".to_string()],
503        };
504        assert_eq!(comp.to_string(), "A ∪ B ∪ C");
505    }
506
507    #[test]
508    fn test_intersection_computation() {
509        let comp = DomainComputation::Intersection {
510            domains: vec!["A".to_string(), "B".to_string()],
511        };
512        assert_eq!(comp.to_string(), "A ∩ B");
513    }
514
515    #[test]
516    fn test_difference_computation() {
517        let comp = DomainComputation::Difference {
518            base: "A".to_string(),
519            subtract: "B".to_string(),
520        };
521        assert_eq!(comp.to_string(), "A \\ B");
522    }
523
524    #[test]
525    fn test_product_computation() {
526        let comp = DomainComputation::Product {
527            domains: vec!["A".to_string(), "B".to_string()],
528        };
529        assert_eq!(comp.to_string(), "A × B");
530    }
531
532    #[test]
533    fn test_powerset_computation() {
534        let comp = DomainComputation::PowerSet {
535            base: "A".to_string(),
536        };
537        assert_eq!(comp.to_string(), "℘(A)");
538    }
539
540    #[test]
541    fn test_computed_domain_creation() {
542        let domain = ComputedDomain::new(
543            "Adults",
544            DomainComputation::Filter {
545                base: "Person".to_string(),
546                predicate: "is_adult".to_string(),
547            },
548        );
549        assert_eq!(domain.name(), "Adults");
550        assert!(!domain.is_materialized());
551    }
552
553    #[test]
554    fn test_cardinality_estimate() {
555        let domain = ComputedDomain::new(
556            "Adults",
557            DomainComputation::Filter {
558                base: "Person".to_string(),
559                predicate: "is_adult".to_string(),
560            },
561        )
562        .with_cardinality_estimate(750);
563        assert_eq!(domain.cardinality_estimate(), Some(750));
564    }
565
566    #[test]
567    fn test_cardinality_bounds_filter() {
568        let mut table = SymbolTable::new();
569        table
570            .add_domain(DomainInfo::new("Person", 1000))
571            .expect("unwrap");
572        table
573            .add_predicate(PredicateInfo::new("is_adult", vec!["Person".to_string()]))
574            .expect("unwrap");
575
576        let domain = ComputedDomain::new(
577            "Adults",
578            DomainComputation::Filter {
579                base: "Person".to_string(),
580                predicate: "is_adult".to_string(),
581            },
582        );
583
584        let (lower, upper) = domain.cardinality_bounds(&table).expect("unwrap");
585        assert_eq!(lower, 0);
586        assert_eq!(upper, 1000);
587    }
588
589    #[test]
590    fn test_cardinality_bounds_union() {
591        let mut table = SymbolTable::new();
592        table.add_domain(DomainInfo::new("A", 100)).expect("unwrap");
593        table.add_domain(DomainInfo::new("B", 200)).expect("unwrap");
594
595        let domain = ComputedDomain::new(
596            "AorB",
597            DomainComputation::Union {
598                domains: vec!["A".to_string(), "B".to_string()],
599            },
600        );
601
602        let (lower, upper) = domain.cardinality_bounds(&table).expect("unwrap");
603        assert_eq!(lower, 200); // max(100, 200)
604        assert_eq!(upper, 300); // 100 + 200
605    }
606
607    #[test]
608    fn test_cardinality_bounds_product() {
609        let mut table = SymbolTable::new();
610        table.add_domain(DomainInfo::new("A", 10)).expect("unwrap");
611        table.add_domain(DomainInfo::new("B", 20)).expect("unwrap");
612
613        let domain = ComputedDomain::new(
614            "AxB",
615            DomainComputation::Product {
616                domains: vec!["A".to_string(), "B".to_string()],
617            },
618        );
619
620        let (lower, upper) = domain.cardinality_bounds(&table).expect("unwrap");
621        assert_eq!(lower, 200);
622        assert_eq!(upper, 200);
623    }
624
625    #[test]
626    fn test_validate_success() {
627        let mut table = SymbolTable::new();
628        table
629            .add_domain(DomainInfo::new("Person", 1000))
630            .expect("unwrap");
631        table
632            .add_predicate(PredicateInfo::new("is_adult", vec!["Person".to_string()]))
633            .expect("unwrap");
634
635        let domain = ComputedDomain::new(
636            "Adults",
637            DomainComputation::Filter {
638                base: "Person".to_string(),
639                predicate: "is_adult".to_string(),
640            },
641        );
642
643        assert!(domain.validate(&table).is_ok());
644    }
645
646    #[test]
647    fn test_validate_missing_domain() {
648        let table = SymbolTable::new();
649
650        let domain = ComputedDomain::new(
651            "Adults",
652            DomainComputation::Filter {
653                base: "Person".to_string(),
654                predicate: "is_adult".to_string(),
655            },
656        );
657
658        assert!(domain.validate(&table).is_err());
659    }
660
661    #[test]
662    fn test_registry_register() {
663        let mut registry = ComputedDomainRegistry::new();
664        let domain = ComputedDomain::new(
665            "Adults",
666            DomainComputation::Filter {
667                base: "Person".to_string(),
668                predicate: "is_adult".to_string(),
669            },
670        );
671        assert!(registry.register(domain).is_ok());
672        assert_eq!(registry.len(), 1);
673    }
674
675    #[test]
676    fn test_registry_duplicate() {
677        let mut registry = ComputedDomainRegistry::new();
678        let domain1 = ComputedDomain::new(
679            "Adults",
680            DomainComputation::Filter {
681                base: "Person".to_string(),
682                predicate: "is_adult".to_string(),
683            },
684        );
685        let domain2 = ComputedDomain::new(
686            "Adults",
687            DomainComputation::Filter {
688                base: "Person".to_string(),
689                predicate: "other".to_string(),
690            },
691        );
692        registry.register(domain1).expect("unwrap");
693        assert!(registry.register(domain2).is_err());
694    }
695
696    #[test]
697    fn test_registry_get() {
698        let mut registry = ComputedDomainRegistry::new();
699        let domain = ComputedDomain::new(
700            "Adults",
701            DomainComputation::Filter {
702                base: "Person".to_string(),
703                predicate: "is_adult".to_string(),
704            },
705        );
706        registry.register(domain).expect("unwrap");
707
708        assert!(registry.get("Adults").is_some());
709        assert!(registry.get("Unknown").is_none());
710    }
711
712    #[test]
713    fn test_registry_remove() {
714        let mut registry = ComputedDomainRegistry::new();
715        let domain = ComputedDomain::new(
716            "Adults",
717            DomainComputation::Filter {
718                base: "Person".to_string(),
719                predicate: "is_adult".to_string(),
720            },
721        );
722        registry.register(domain).expect("unwrap");
723
724        assert!(registry.remove("Adults").is_some());
725        assert_eq!(registry.len(), 0);
726    }
727
728    #[test]
729    fn test_display() {
730        let domain = ComputedDomain::new(
731            "Adults",
732            DomainComputation::Filter {
733                base: "Person".to_string(),
734                predicate: "is_adult".to_string(),
735            },
736        );
737        let s = format!("{}", domain);
738        assert!(s.contains("Adults"));
739        assert!(s.contains(":="));
740    }
741}