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| table.get_domain(d).unwrap().cardinality)
256                    .sum();
257                Ok((max_card, sum_card))
258            }
259            DomainComputation::Intersection { domains } => {
260                let min_card = domains
261                    .iter()
262                    .map(|d| {
263                        table
264                            .get_domain(d)
265                            .map(|info| info.cardinality)
266                            .ok_or_else(|| AdapterError::UnknownDomain(d.clone()))
267                    })
268                    .collect::<Result<Vec<_>, _>>()?
269                    .into_iter()
270                    .min()
271                    .unwrap_or(0);
272                Ok((0, min_card))
273            }
274            DomainComputation::Difference { base, subtract } => {
275                let base_card = table
276                    .get_domain(base)
277                    .ok_or_else(|| AdapterError::UnknownDomain(base.clone()))?
278                    .cardinality;
279                let sub_card = table
280                    .get_domain(subtract)
281                    .ok_or_else(|| AdapterError::UnknownDomain(subtract.clone()))?
282                    .cardinality;
283                Ok((0, base_card.saturating_sub(sub_card)))
284            }
285            DomainComputation::Product { domains } => {
286                let product: usize = domains
287                    .iter()
288                    .map(|d| {
289                        table
290                            .get_domain(d)
291                            .map(|info| info.cardinality)
292                            .ok_or_else(|| AdapterError::UnknownDomain(d.clone()))
293                    })
294                    .collect::<Result<Vec<_>, _>>()?
295                    .into_iter()
296                    .product();
297                Ok((product, product))
298            }
299            DomainComputation::PowerSet { base } => {
300                let base_card = table
301                    .get_domain(base)
302                    .ok_or_else(|| AdapterError::UnknownDomain(base.clone()))?
303                    .cardinality;
304                let power = 2_usize.pow(base_card as u32);
305                Ok((power, power))
306            }
307            DomainComputation::Projection { product, index: _ } => {
308                let prod_domain = table
309                    .get_domain(product)
310                    .ok_or_else(|| AdapterError::UnknownDomain(product.clone()))?;
311                // This is simplified - in real implementation we'd parse product structure
312                Ok((0, prod_domain.cardinality))
313            }
314            DomainComputation::Custom { .. } => {
315                // For custom computations, use estimate if available
316                if let Some(est) = self.cardinality_estimate {
317                    Ok((0, est))
318                } else {
319                    Err(AdapterError::InvalidOperation(
320                        "Custom domain computation requires cardinality estimate".to_string(),
321                    ))
322                }
323            }
324        }
325    }
326
327    /// Validate that all dependencies exist in the symbol table.
328    pub fn validate(&self, table: &SymbolTable) -> Result<(), AdapterError> {
329        match &self.computation {
330            DomainComputation::Filter { base, predicate } => {
331                if table.get_domain(base).is_none() {
332                    return Err(AdapterError::UnknownDomain(base.clone()));
333                }
334                if table.get_predicate(predicate).is_none() {
335                    return Err(AdapterError::UnknownPredicate(predicate.clone()));
336                }
337            }
338            DomainComputation::Union { domains } | DomainComputation::Intersection { domains } => {
339                for domain in domains {
340                    if table.get_domain(domain).is_none() {
341                        return Err(AdapterError::UnknownDomain(domain.clone()));
342                    }
343                }
344            }
345            DomainComputation::Difference { base, subtract } => {
346                if table.get_domain(base).is_none() {
347                    return Err(AdapterError::UnknownDomain(base.clone()));
348                }
349                if table.get_domain(subtract).is_none() {
350                    return Err(AdapterError::UnknownDomain(subtract.clone()));
351                }
352            }
353            DomainComputation::Product { domains } => {
354                for domain in domains {
355                    if table.get_domain(domain).is_none() {
356                        return Err(AdapterError::UnknownDomain(domain.clone()));
357                    }
358                }
359            }
360            DomainComputation::PowerSet { base } => {
361                if table.get_domain(base).is_none() {
362                    return Err(AdapterError::UnknownDomain(base.clone()));
363                }
364            }
365            DomainComputation::Projection { product, .. } => {
366                if table.get_domain(product).is_none() {
367                    return Err(AdapterError::UnknownDomain(product.clone()));
368                }
369            }
370            DomainComputation::Custom { .. } => {
371                // Custom computations can't be validated automatically
372            }
373        }
374        Ok(())
375    }
376
377    /// Mark this domain as materialized.
378    pub fn materialize(&mut self) {
379        self.materialized = true;
380    }
381}
382
383impl fmt::Display for ComputedDomain {
384    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
385        write!(f, "{} := {}", self.name, self.computation)
386    }
387}
388
389/// Registry for managing computed domains.
390///
391/// The registry tracks computed domains and their dependencies,
392/// enabling lazy evaluation and caching.
393#[derive(Clone, Debug, Default)]
394pub struct ComputedDomainRegistry {
395    /// Registered computed domains.
396    domains: HashMap<String, ComputedDomain>,
397}
398
399impl ComputedDomainRegistry {
400    /// Create a new empty registry.
401    pub fn new() -> Self {
402        Self {
403            domains: HashMap::new(),
404        }
405    }
406
407    /// Register a computed domain.
408    ///
409    /// # Examples
410    ///
411    /// ```rust
412    /// use tensorlogic_adapters::{ComputedDomainRegistry, ComputedDomain, DomainComputation};
413    ///
414    /// let mut registry = ComputedDomainRegistry::new();
415    /// let domain = ComputedDomain::new(
416    ///     "Adults",
417    ///     DomainComputation::Filter {
418    ///         base: "Person".to_string(),
419    ///         predicate: "is_adult".to_string(),
420    ///     }
421    /// );
422    /// registry.register(domain).unwrap();
423    /// ```
424    pub fn register(&mut self, domain: ComputedDomain) -> Result<(), AdapterError> {
425        let name = domain.name().to_string();
426        if self.domains.contains_key(&name) {
427            return Err(AdapterError::DuplicateDomain(name));
428        }
429        self.domains.insert(name, domain);
430        Ok(())
431    }
432
433    /// Get a computed domain by name.
434    pub fn get(&self, name: &str) -> Option<&ComputedDomain> {
435        self.domains.get(name)
436    }
437
438    /// Get a mutable reference to a computed domain.
439    pub fn get_mut(&mut self, name: &str) -> Option<&mut ComputedDomain> {
440        self.domains.get_mut(name)
441    }
442
443    /// List all registered computed domains.
444    pub fn list(&self) -> Vec<&ComputedDomain> {
445        self.domains.values().collect()
446    }
447
448    /// Remove a computed domain from the registry.
449    pub fn remove(&mut self, name: &str) -> Option<ComputedDomain> {
450        self.domains.remove(name)
451    }
452
453    /// Validate all computed domains against a symbol table.
454    pub fn validate_all(&self, table: &SymbolTable) -> Result<(), Vec<AdapterError>> {
455        let errors: Vec<_> = self
456            .domains
457            .values()
458            .filter_map(|domain| domain.validate(table).err())
459            .collect();
460
461        if errors.is_empty() {
462            Ok(())
463        } else {
464            Err(errors)
465        }
466    }
467
468    /// Get the number of registered domains.
469    pub fn len(&self) -> usize {
470        self.domains.len()
471    }
472
473    /// Check if the registry is empty.
474    pub fn is_empty(&self) -> bool {
475        self.domains.is_empty()
476    }
477}
478
479#[cfg(test)]
480mod tests {
481    use super::*;
482    use crate::{DomainInfo, PredicateInfo};
483
484    #[test]
485    fn test_filter_computation() {
486        let comp = DomainComputation::Filter {
487            base: "Person".to_string(),
488            predicate: "is_adult".to_string(),
489        };
490        assert!(comp.to_string().contains("Person"));
491        assert!(comp.to_string().contains("is_adult"));
492    }
493
494    #[test]
495    fn test_union_computation() {
496        let comp = DomainComputation::Union {
497            domains: vec!["A".to_string(), "B".to_string(), "C".to_string()],
498        };
499        assert_eq!(comp.to_string(), "A ∪ B ∪ C");
500    }
501
502    #[test]
503    fn test_intersection_computation() {
504        let comp = DomainComputation::Intersection {
505            domains: vec!["A".to_string(), "B".to_string()],
506        };
507        assert_eq!(comp.to_string(), "A ∩ B");
508    }
509
510    #[test]
511    fn test_difference_computation() {
512        let comp = DomainComputation::Difference {
513            base: "A".to_string(),
514            subtract: "B".to_string(),
515        };
516        assert_eq!(comp.to_string(), "A \\ B");
517    }
518
519    #[test]
520    fn test_product_computation() {
521        let comp = DomainComputation::Product {
522            domains: vec!["A".to_string(), "B".to_string()],
523        };
524        assert_eq!(comp.to_string(), "A × B");
525    }
526
527    #[test]
528    fn test_powerset_computation() {
529        let comp = DomainComputation::PowerSet {
530            base: "A".to_string(),
531        };
532        assert_eq!(comp.to_string(), "℘(A)");
533    }
534
535    #[test]
536    fn test_computed_domain_creation() {
537        let domain = ComputedDomain::new(
538            "Adults",
539            DomainComputation::Filter {
540                base: "Person".to_string(),
541                predicate: "is_adult".to_string(),
542            },
543        );
544        assert_eq!(domain.name(), "Adults");
545        assert!(!domain.is_materialized());
546    }
547
548    #[test]
549    fn test_cardinality_estimate() {
550        let domain = ComputedDomain::new(
551            "Adults",
552            DomainComputation::Filter {
553                base: "Person".to_string(),
554                predicate: "is_adult".to_string(),
555            },
556        )
557        .with_cardinality_estimate(750);
558        assert_eq!(domain.cardinality_estimate(), Some(750));
559    }
560
561    #[test]
562    fn test_cardinality_bounds_filter() {
563        let mut table = SymbolTable::new();
564        table.add_domain(DomainInfo::new("Person", 1000)).unwrap();
565        table
566            .add_predicate(PredicateInfo::new("is_adult", vec!["Person".to_string()]))
567            .unwrap();
568
569        let domain = ComputedDomain::new(
570            "Adults",
571            DomainComputation::Filter {
572                base: "Person".to_string(),
573                predicate: "is_adult".to_string(),
574            },
575        );
576
577        let (lower, upper) = domain.cardinality_bounds(&table).unwrap();
578        assert_eq!(lower, 0);
579        assert_eq!(upper, 1000);
580    }
581
582    #[test]
583    fn test_cardinality_bounds_union() {
584        let mut table = SymbolTable::new();
585        table.add_domain(DomainInfo::new("A", 100)).unwrap();
586        table.add_domain(DomainInfo::new("B", 200)).unwrap();
587
588        let domain = ComputedDomain::new(
589            "AorB",
590            DomainComputation::Union {
591                domains: vec!["A".to_string(), "B".to_string()],
592            },
593        );
594
595        let (lower, upper) = domain.cardinality_bounds(&table).unwrap();
596        assert_eq!(lower, 200); // max(100, 200)
597        assert_eq!(upper, 300); // 100 + 200
598    }
599
600    #[test]
601    fn test_cardinality_bounds_product() {
602        let mut table = SymbolTable::new();
603        table.add_domain(DomainInfo::new("A", 10)).unwrap();
604        table.add_domain(DomainInfo::new("B", 20)).unwrap();
605
606        let domain = ComputedDomain::new(
607            "AxB",
608            DomainComputation::Product {
609                domains: vec!["A".to_string(), "B".to_string()],
610            },
611        );
612
613        let (lower, upper) = domain.cardinality_bounds(&table).unwrap();
614        assert_eq!(lower, 200);
615        assert_eq!(upper, 200);
616    }
617
618    #[test]
619    fn test_validate_success() {
620        let mut table = SymbolTable::new();
621        table.add_domain(DomainInfo::new("Person", 1000)).unwrap();
622        table
623            .add_predicate(PredicateInfo::new("is_adult", vec!["Person".to_string()]))
624            .unwrap();
625
626        let domain = ComputedDomain::new(
627            "Adults",
628            DomainComputation::Filter {
629                base: "Person".to_string(),
630                predicate: "is_adult".to_string(),
631            },
632        );
633
634        assert!(domain.validate(&table).is_ok());
635    }
636
637    #[test]
638    fn test_validate_missing_domain() {
639        let table = SymbolTable::new();
640
641        let domain = ComputedDomain::new(
642            "Adults",
643            DomainComputation::Filter {
644                base: "Person".to_string(),
645                predicate: "is_adult".to_string(),
646            },
647        );
648
649        assert!(domain.validate(&table).is_err());
650    }
651
652    #[test]
653    fn test_registry_register() {
654        let mut registry = ComputedDomainRegistry::new();
655        let domain = ComputedDomain::new(
656            "Adults",
657            DomainComputation::Filter {
658                base: "Person".to_string(),
659                predicate: "is_adult".to_string(),
660            },
661        );
662        assert!(registry.register(domain).is_ok());
663        assert_eq!(registry.len(), 1);
664    }
665
666    #[test]
667    fn test_registry_duplicate() {
668        let mut registry = ComputedDomainRegistry::new();
669        let domain1 = ComputedDomain::new(
670            "Adults",
671            DomainComputation::Filter {
672                base: "Person".to_string(),
673                predicate: "is_adult".to_string(),
674            },
675        );
676        let domain2 = ComputedDomain::new(
677            "Adults",
678            DomainComputation::Filter {
679                base: "Person".to_string(),
680                predicate: "other".to_string(),
681            },
682        );
683        registry.register(domain1).unwrap();
684        assert!(registry.register(domain2).is_err());
685    }
686
687    #[test]
688    fn test_registry_get() {
689        let mut registry = ComputedDomainRegistry::new();
690        let domain = ComputedDomain::new(
691            "Adults",
692            DomainComputation::Filter {
693                base: "Person".to_string(),
694                predicate: "is_adult".to_string(),
695            },
696        );
697        registry.register(domain).unwrap();
698
699        assert!(registry.get("Adults").is_some());
700        assert!(registry.get("Unknown").is_none());
701    }
702
703    #[test]
704    fn test_registry_remove() {
705        let mut registry = ComputedDomainRegistry::new();
706        let domain = ComputedDomain::new(
707            "Adults",
708            DomainComputation::Filter {
709                base: "Person".to_string(),
710                predicate: "is_adult".to_string(),
711            },
712        );
713        registry.register(domain).unwrap();
714
715        assert!(registry.remove("Adults").is_some());
716        assert_eq!(registry.len(), 0);
717    }
718
719    #[test]
720    fn test_display() {
721        let domain = ComputedDomain::new(
722            "Adults",
723            DomainComputation::Filter {
724                base: "Person".to_string(),
725                predicate: "is_adult".to_string(),
726            },
727        );
728        let s = format!("{}", domain);
729        assert!(s.contains("Adults"));
730        assert!(s.contains(":="));
731    }
732}