Skip to main content

polydat_grammar/comprehension/
source.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Clause source values — spec §3.1.
5//!
6//! A `clause(name, source)` binds a name to the values
7//! produced by its source. Sources split into two families:
8//!
9//! - **Discrete stream producers** — literal lists, integer
10//!   ranges, generator functions, workload-param references.
11//!   Cardinality is `Bounded`, `BoundedAtMost`, or `Unbounded`.
12//! - **Continuous measures** — real intervals with an
13//!   integrable measure (uniform on bounded intervals; named
14//!   probability distributions like Normal / Exponential).
15//!   Cardinality is `Continuous`; V8 requires an enclosing
16//!   sampling `order(_, strategy, Some(n))` before dispense.
17//!
18//! Sources are stream producers — they do not pre-materialize
19//! into `Vec<Value>`. This is the load-bearing model property
20//! per spec §3.1 + §6.2.
21
22use serde::{Deserialize, Serialize};
23
24use super::cardinality::{CardinalityClass, Interval, MeasureName, ProductMeasure};
25
26/// A clause's source of values.
27///
28/// Discrete variants produce a stream of `Value` via the
29/// runtime evaluator; continuous variants describe a measure
30/// that a downstream sampling strategy will draw from.
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32#[serde(tag = "kind", rename_all = "snake_case")]
33pub enum Source {
34    /// Literal comma list (e.g., `[1, 2, 4, 8]`). Stream
35    /// producer over the list contents.
36    Literal {
37        /// The values, in order.
38        values: Vec<LiteralValue>,
39    },
40
41    /// Integer half-open range `lo..hi` with optional step.
42    /// Default step is 1.
43    IntRange {
44        /// The first value.
45        lo: i64,
46        /// One past the last.
47        hi: i64,
48        /// The step between values, 1 by default.
49        step: i64,
50    },
51
52    /// Generator function call expressed as a Polydat source string.
53    /// Resolved at clause construction; cardinality may be
54    /// `Unbounded` if the generator is open-ended.
55    Generator {
56        /// The generator call, as Polydat source.
57        expr: String,
58        /// How many values it yields, when known.
59        cardinality_hint: Option<u64>,
60    },
61
62    /// Reference to a workload-level parameter that resolves to
63    /// a list of values. Cardinality is the parameter's
64    /// declared list length.
65    WorkloadParamList {
66        /// The parameter's name.
67        name: String,
68        /// The list's length, when known.
69        len_hint: Option<u64>,
70    },
71
72    /// Real interval (continuous source). Combined with a
73    /// `measure` to form a `Continuous` cardinality.
74    /// Integrability is checked at parse via V8.
75    ContinuousInterval {
76        /// The interval.
77        interval: Interval,
78        /// The measure drawn from.
79        measure: ProductMeasure,
80    },
81
82    /// Named continuous distribution. The distribution carries
83    /// its own support; the `support` field records the
84    /// effective interval for V8's check.
85    Distribution {
86        /// The distribution.
87        distribution: MeasureName,
88        /// Its effective support.
89        support: Interval,
90        /// Its parameters, in the distribution's order.
91        params: Vec<f64>,
92    },
93}
94
95/// A literal value carried in a `Source::Literal`. Subset of
96/// the polydat `Value` type — the kinds clauses can directly
97/// bind. Extension to richer value types lives in the source
98/// evaluator, not the AST.
99///
100/// Serialized untagged because the variants are primitives;
101/// the JSON/YAML representation is just the bare value
102/// (`1` / `"x"` / `true` / `1.5`).
103#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
104#[serde(untagged)]
105pub enum LiteralValue {
106    /// An integer.
107    Int(i64),
108    /// A float.
109    Float(f64),
110    /// A string.
111    String(String),
112    /// A boolean.
113    Bool(bool),
114    /// A JSON value carrying its own kind: an item of a JSON list a
115    /// generator supplied at run time, bound where the element is
116    /// declared `json`. Last, so an untagged read tries the scalar
117    /// forms first.
118    Json(serde_json::Value),
119}
120
121impl Source {
122    /// Declare this source's cardinality class for use by
123    /// `clause` metadata propagation.
124    pub fn cardinality(&self) -> CardinalityClass {
125        match self {
126            Source::Literal { values } => CardinalityClass::Bounded(values.len() as u64),
127            Source::IntRange { lo, hi, step } => {
128                let step = (*step).max(1).unsigned_abs();
129                if hi <= lo {
130                    CardinalityClass::Bounded(0)
131                } else {
132                    let span = (hi - lo) as u64;
133                    let n = span.div_ceil(step);
134                    CardinalityClass::Bounded(n)
135                }
136            }
137            Source::Generator {
138                cardinality_hint, ..
139            } => match cardinality_hint {
140                Some(n) => CardinalityClass::Bounded(*n),
141                None => CardinalityClass::Unbounded,
142            },
143            Source::WorkloadParamList { len_hint, .. } => match len_hint {
144                Some(n) => CardinalityClass::Bounded(*n),
145                None => CardinalityClass::Unbounded,
146            },
147            Source::ContinuousInterval { interval, measure } => CardinalityClass::Continuous {
148                intervals: vec![interval.clone()],
149                measure: measure.clone(),
150            },
151            Source::Distribution { support, .. } => CardinalityClass::Continuous {
152                intervals: vec![support.clone()],
153                measure: ProductMeasure::Named(*self.distribution_name()),
154            },
155        }
156    }
157
158    /// `true` if this source is continuous (Continuous /
159    /// Distribution variants). Used by V7 (zip must be all
160    /// discrete) and V9 (union must be all discrete) without
161    /// a full cardinality computation.
162    pub fn is_continuous(&self) -> bool {
163        matches!(
164            self,
165            Source::ContinuousInterval { .. } | Source::Distribution { .. }
166        )
167    }
168
169    /// `true` if this source is discrete (every variant except
170    /// the continuous ones).
171    pub fn is_discrete(&self) -> bool {
172        !self.is_continuous()
173    }
174
175    fn distribution_name(&self) -> &MeasureName {
176        match self {
177            Source::Distribution { distribution, .. } => distribution,
178            _ => panic!("distribution_name called on non-Distribution source"),
179        }
180    }
181}
182
183// ── SRD-18f: iteration interior + string-comprehension striping ──
184
185/// The SRD-18f string-comprehension separator rule, in one place
186/// so the parse-time (`source_parser`) and runtime (`eval`)
187/// striping can never drift: split on runs of comma / semicolon /
188/// ASCII whitespace, trim, drop empties. Every other character
189/// (`:` `.` `-` `/` …) stays in the token. Returns the raw token
190/// substrings; callers type them (Value or LiteralValue).
191pub fn split_string_comprehension(s: &str) -> Vec<&str> {
192    s.split(|c: char| c == ',' || c == ';' || c.is_ascii_whitespace())
193        .map(str::trim)
194        .filter(|t| !t.is_empty())
195        .collect()
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    #[test]
203    fn literal_cardinality_is_list_length() {
204        let s = Source::Literal {
205            values: vec![
206                LiteralValue::Int(1),
207                LiteralValue::Int(2),
208                LiteralValue::Int(3),
209            ],
210        };
211        assert!(matches!(s.cardinality(), CardinalityClass::Bounded(3)));
212    }
213
214    #[test]
215    fn int_range_step_1() {
216        let s = Source::IntRange {
217            lo: 1,
218            hi: 10,
219            step: 1,
220        };
221        assert!(matches!(s.cardinality(), CardinalityClass::Bounded(9)));
222    }
223
224    #[test]
225    fn int_range_with_step() {
226        let s = Source::IntRange {
227            lo: 0,
228            hi: 10,
229            step: 2,
230        };
231        // 0,2,4,6,8 = 5 values
232        assert!(matches!(s.cardinality(), CardinalityClass::Bounded(5)));
233    }
234
235    #[test]
236    fn int_range_empty() {
237        let s = Source::IntRange {
238            lo: 5,
239            hi: 5,
240            step: 1,
241        };
242        assert!(matches!(s.cardinality(), CardinalityClass::Bounded(0)));
243    }
244
245    #[test]
246    fn generator_without_hint_is_unbounded() {
247        let s = Source::Generator {
248            expr: "live_query()".into(),
249            cardinality_hint: None,
250        };
251        assert!(matches!(s.cardinality(), CardinalityClass::Unbounded));
252    }
253
254    #[test]
255    fn generator_with_hint_is_bounded() {
256        let s = Source::Generator {
257            expr: "first_100()".into(),
258            cardinality_hint: Some(100),
259        };
260        assert!(matches!(s.cardinality(), CardinalityClass::Bounded(100)));
261    }
262
263    #[test]
264    fn continuous_interval_produces_continuous_class() {
265        let s = Source::ContinuousInterval {
266            interval: Interval::closed(0.0, 1.0),
267            measure: ProductMeasure::Uniform,
268        };
269        match s.cardinality() {
270            CardinalityClass::Continuous { intervals, measure } => {
271                assert_eq!(intervals.len(), 1);
272                assert!(matches!(measure, ProductMeasure::Uniform));
273            }
274            other => panic!("expected Continuous, got {other:?}"),
275        }
276        assert!(s.is_continuous());
277        assert!(!s.is_discrete());
278    }
279
280    #[test]
281    fn distribution_source_classification() {
282        let s = Source::Distribution {
283            distribution: MeasureName::Normal,
284            support: Interval {
285                lo: f64::NEG_INFINITY,
286                hi: f64::INFINITY,
287                lo_open: true,
288                hi_open: true,
289            },
290            params: vec![0.0, 1.0],
291        };
292        assert!(s.is_continuous());
293        match s.cardinality() {
294            CardinalityClass::Continuous {
295                measure: ProductMeasure::Named(MeasureName::Normal),
296                ..
297            } => {}
298            other => panic!("expected Continuous with Named(Normal), got {other:?}"),
299        }
300    }
301}