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}