Skip to main content

polydat_grammar/comprehension/
cardinality.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Cardinality classes — spec §6.1.
5//!
6//! Six classes describe every comprehension's dispense count.
7//! Three are discrete (`Bounded`, `BoundedAtMost`, `Unbounded`);
8//! two are continuous-domain (`Continuous`, `ContinuousAtMost`);
9//! one is hybrid (`Hybrid`). The class propagates through every
10//! constructor per spec §6.1's table.
11
12use serde::{Deserialize, Serialize};
13
14/// Cardinality of a comprehension's dispense stream.
15///
16/// Six variants per spec §6.1:
17///
18/// - **Discrete classes** enumerate distinct tuples; the count
19///   may be known exactly (`Bounded`), bounded above
20///   (`BoundedAtMost`), or unknown (`Unbounded`).
21/// - **Continuous classes** describe a measure-theoretic value
22///   space; they cannot enumerate and must be sampled via an
23///   enclosing `order(_, strategy, Some(n))` per V8.
24/// - **Hybrid** is a cartesian whose children mix discrete and
25///   continuous axes.
26#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
27pub enum CardinalityClass {
28    /// Discrete, exactly `n` tuples.
29    Bounded(u64),
30
31    /// Discrete, between 0 and `n` tuples (post-filter).
32    BoundedAtMost(u64),
33
34    /// Discrete, no known upper bound (generator, live stream).
35    Unbounded,
36
37    /// Continuous source — bounded or unbounded real intervals
38    /// with an integrable product measure. Sampled rather than
39    /// enumerated; V8 requires an enclosing
40    /// `order(_, strategy, Some(n))` before reaching a
41    /// `PolyStreamer`.
42    Continuous {
43        /// The interval of each axis.
44        intervals: Vec<Interval>,
45        /// The measure sampled.
46        measure: ProductMeasure,
47    },
48
49    /// Filtered continuous source. Measure reduced by the
50    /// predicate; still requires sampling.
51    ContinuousAtMost {
52        /// The interval of each axis.
53        intervals: Vec<Interval>,
54        /// The measure before the predicate reduces it.
55        measure_at_most: ProductMeasure,
56    },
57
58    /// Mixed discrete × continuous cartesian. The discrete part
59    /// is enumerable; the continuous part needs sampling. V8
60    /// applies to the continuous component.
61    Hybrid(Hybrid),
62}
63
64/// Mixed discrete × continuous cartesian shape.
65///
66/// Each `discrete_axes` entry is the axis size in tuples; each
67/// `continuous_axes` entry is the interval the axis spans.
68/// `measure` covers the continuous part.
69#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
70pub struct Hybrid {
71    /// Per-axis cardinality for the discrete axes, in
72    /// declaration order.
73    pub discrete_axes: Vec<u64>,
74    /// Per-axis intervals for the continuous axes, in
75    /// declaration order.
76    pub continuous_axes: Vec<Interval>,
77    /// Product measure over the continuous axes.
78    pub measure: ProductMeasure,
79}
80
81/// Real interval `[lo, hi]` (or open variants) for continuous
82/// sources. Unbounded sides use `f64::NEG_INFINITY` /
83/// `f64::INFINITY`; the V8 integrability check determines
84/// whether such intervals are valid given the measure.
85#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
86pub struct Interval {
87    /// The lower end.
88    pub lo: f64,
89    /// The upper end.
90    pub hi: f64,
91    /// Whether the lower end is excluded.
92    pub lo_open: bool,
93    /// Whether the upper end is excluded.
94    pub hi_open: bool,
95}
96
97impl Interval {
98    /// Closed-closed interval `[lo, hi]`.
99    pub fn closed(lo: f64, hi: f64) -> Self {
100        Self {
101            lo,
102            hi,
103            lo_open: false,
104            hi_open: false,
105        }
106    }
107
108    /// Half-open `[lo, hi)`.
109    pub fn half_open(lo: f64, hi: f64) -> Self {
110        Self {
111            lo,
112            hi,
113            lo_open: false,
114            hi_open: true,
115        }
116    }
117
118    /// Open interval `(lo, hi)`.
119    pub fn open(lo: f64, hi: f64) -> Self {
120        Self {
121            lo,
122            hi,
123            lo_open: true,
124            hi_open: true,
125        }
126    }
127
128    /// `true` if the interval has finite Lebesgue measure
129    /// (both endpoints finite). Used by V8's integrability
130    /// check together with the measure variant.
131    pub fn is_bounded(&self) -> bool {
132        self.lo.is_finite() && self.hi.is_finite()
133    }
134}
135
136/// Product measure over one or more continuous axes.
137///
138/// `Uniform` is the Lebesgue measure scaled by interval width
139/// (requires bounded intervals; V8 rejects unbounded + Uniform).
140/// `Named(D)` is a probability distribution with proper density
141/// over its declared support — Normal, Exponential, Pareto,
142/// Beta, etc. `Product(_)` carries a per-axis product of measures
143/// for K-D continuous cartesians.
144#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
145pub enum ProductMeasure {
146    /// Lebesgue measure scaled to the interval; needs bounded intervals.
147    Uniform,
148    /// A named probability distribution over its support.
149    Named(MeasureName),
150    /// One measure per axis.
151    Product(Vec<ProductMeasure>),
152}
153
154impl ProductMeasure {
155    /// `true` if this measure has finite total mass given the
156    /// supplied intervals. Used by V8.
157    ///
158    /// - `Uniform` is integrable iff every interval is bounded.
159    /// - `Named(D)` is integrable per its distribution: proper
160    ///   probability distributions are always integrable
161    ///   (they have unit total mass by definition).
162    /// - `Product(children)` is integrable iff every child is.
163    pub fn is_integrable(&self, intervals: &[Interval]) -> bool {
164        match self {
165            ProductMeasure::Uniform => intervals.iter().all(Interval::is_bounded),
166            ProductMeasure::Named(name) => name.is_proper_probability_measure(),
167            ProductMeasure::Product(children) => {
168                if children.len() != intervals.len() {
169                    return false;
170                }
171                children
172                    .iter()
173                    .zip(intervals.iter())
174                    .all(|(m, i)| m.is_integrable(std::slice::from_ref(i)))
175            }
176        }
177    }
178}
179
180/// Named continuous distribution. Closed enum per spec
181/// §10.7.5's "User-defined extensions" non-goal — new
182/// distributions land as coordinated additions.
183#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
184pub enum MeasureName {
185    /// The normal distribution.
186    Normal,
187    /// The exponential distribution.
188    Exponential,
189    /// The Pareto distribution.
190    Pareto,
191    /// The beta distribution.
192    Beta,
193    /// The log-normal distribution.
194    LogNormal,
195    /// The gamma distribution.
196    Gamma,
197    /// The uniform distribution on `[0, 1]`.
198    Uniform01,
199}
200
201impl MeasureName {
202    /// All currently-named distributions are proper probability
203    /// measures (unit total mass). V8 accepts them on any
204    /// interval that matches the distribution's support.
205    pub fn is_proper_probability_measure(self) -> bool {
206        true
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn bounded_interval_is_bounded() {
216        assert!(Interval::closed(0.0, 1.0).is_bounded());
217        assert!(Interval::open(-1.0, 1.0).is_bounded());
218    }
219
220    #[test]
221    fn unbounded_interval_is_not_bounded() {
222        let i = Interval {
223            lo: 0.0,
224            hi: f64::INFINITY,
225            lo_open: false,
226            hi_open: true,
227        };
228        assert!(!i.is_bounded());
229    }
230
231    #[test]
232    fn uniform_integrable_on_bounded_interval() {
233        let m = ProductMeasure::Uniform;
234        assert!(m.is_integrable(&[Interval::closed(0.0, 1.0)]));
235    }
236
237    #[test]
238    fn uniform_not_integrable_on_unbounded_interval() {
239        let m = ProductMeasure::Uniform;
240        let unbounded = Interval {
241            lo: 0.0,
242            hi: f64::INFINITY,
243            lo_open: false,
244            hi_open: true,
245        };
246        assert!(!m.is_integrable(&[unbounded]));
247    }
248
249    #[test]
250    fn named_measure_always_integrable() {
251        let m = ProductMeasure::Named(MeasureName::Normal);
252        let unbounded = Interval {
253            lo: f64::NEG_INFINITY,
254            hi: f64::INFINITY,
255            lo_open: true,
256            hi_open: true,
257        };
258        assert!(m.is_integrable(&[unbounded]));
259    }
260
261    #[test]
262    fn product_measure_requires_matching_arity() {
263        let m = ProductMeasure::Product(vec![ProductMeasure::Uniform, ProductMeasure::Uniform]);
264        assert!(m.is_integrable(&[Interval::closed(0.0, 1.0), Interval::closed(0.0, 1.0)]));
265        assert!(!m.is_integrable(&[Interval::closed(0.0, 1.0)]));
266    }
267
268    #[test]
269    fn cardinality_class_round_trip_serde() {
270        let c = CardinalityClass::Continuous {
271            intervals: vec![Interval::closed(0.0, 1.0)],
272            measure: ProductMeasure::Uniform,
273        };
274        let json = serde_json::to_string(&c).unwrap();
275        let back: CardinalityClass = serde_json::from_str(&json).unwrap();
276        assert_eq!(c, back);
277    }
278}