Skip to main content

polydat_grammar/comprehension/spec/
serde_form.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! [`ComprehensionSpec`] — author-friendly serde-deserializable
5//! form, plus its conversion into the algebra-layer AST.
6//!
7//! The friendly form has a **single `for` verb** (per user
8//! direction) whose RHS shape determines the constructor:
9//!
10//! - `for: "k in 1..10"` — single clause / inline cartesian
11//! - `for: ["k in 1..10", "limit in [1, 2, 3]"]` — list of
12//!   clauses, cartesian (or union if names repeat)
13//! - `for: [["k in 10", "limit in 1..5"], ["k in 100", "limit
14//!   in 1..50"]]` — explicit union of cartesian sub-spaces
15//!
16//! With optional modifiers:
17//!
18//! - `where: "..."` — filter predicate
19//! - `order: "halton/50"` — traversal order spec
20//!
21//! Conversion delegates to the clause parsers, which build the flat
22//! parse form (comprehension_forms.md §14.8), and the
23//! [`super::from_clauses`] lowering. This module owns only the
24//! serde surface and the shape-routing logic.
25
26use serde::{Deserialize, Serialize};
27
28use crate::comprehension::ast::Comprehension as AlgebraAst;
29use crate::comprehension::clause_ast::{Clause as LegacyClause, Comprehension as ClauseAst};
30use crate::comprehension::parse::{
31    comprehension_from_subspaces, parse_clause_list, parse_order_spec,
32};
33
34use super::from_clauses::{ConvertError, clauses_to_algebra};
35
36/// The friendly, serde-deserializable comprehension surface.
37///
38/// Field names match the YAML / JSON keys 1:1. The
39/// `for` field carries the only required input
40/// — the clause specification — in any of the three accepted
41/// shapes (see [`ForSpec`]).
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ComprehensionSpec {
44    /// The clause specification.  See [`ForSpec`] for the
45    /// accepted shapes.  `r#for` because `for` is a reserved
46    /// keyword in Rust — serde renames it for YAML / JSON.
47    #[serde(rename = "for")]
48    pub r#for: ForSpec,
49    /// Optional filter predicate (the `where` clause). String
50    /// form, evaluated against bound coordinates at iteration
51    /// time.
52    #[serde(default, rename = "where", skip_serializing_if = "Option::is_none")]
53    pub r#where: Option<String>,
54    /// Optional traversal-order spec. See
55    /// the crate-internal `parse_order_spec` for
56    /// the accepted syntax (`lex`, `halton/50`,
57    /// `shells(origin=center, depth=3)`, etc.).
58    #[serde(default, skip_serializing_if = "Option::is_none")]
59    pub order: Option<String>,
60}
61
62/// The three accepted shapes of the `for` field.
63///
64/// Routes through the clause parser:
65///
66/// - [`ForSpec::Inline`] → one call to `parse_clause_list`,
67///   then the structural-detection rule (`comprehension_from_subspaces`).
68/// - [`ForSpec::ClauseList`] → one `parse_clause_list` per
69///   entry; each entry becomes its own sub-space (so name
70///   repetition across entries triggers Union per the rule).
71/// - [`ForSpec::UnionOfClauseLists`] → one `parse_clause_list`
72///   per inner list; each inner list is one sub-space.
73#[derive(Debug, Clone, Serialize, Deserialize)]
74#[serde(untagged)]
75pub enum ForSpec {
76    /// `for: "k in 1..10, limit in [1, 2, 3]"` — one inline
77    /// string, possibly multi-clause.
78    Inline(String),
79    /// `for: ["k in 1..10", "limit in [1, 2, 3]"]` — each
80    /// entry is one clause's text.
81    ClauseList(Vec<String>),
82    /// `for: [["k in 10", "limit in 1..5"], …]` — each inner
83    /// list is one sub-space (cartesian over those clauses).
84    UnionOfClauseLists(Vec<Vec<String>>),
85}
86
87/// Errors produced when converting a [`ComprehensionSpec`] to
88/// the algebra-layer AST.
89#[derive(Debug, Clone)]
90pub enum SpecConvertError {
91    /// `parse_clause_list` failed on one of the input strings.
92    ParseClause {
93        /// The clause text.
94        input: String,
95        /// The parser's message.
96        message: String,
97    },
98    /// `parse_order_spec` failed on the `order` field.
99    ParseOrder {
100        /// The order text.
101        input: String,
102        /// The parser's message.
103        message: String,
104    },
105    /// The flat clause form failed its own validation.
106    LegacyValidate {
107        /// The validation errors, in order.
108        errors: Vec<String>,
109    },
110    /// Lowering the flat clause form to the algebra AST failed.
111    Convert(ConvertError),
112}
113
114impl std::fmt::Display for SpecConvertError {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        match self {
117            SpecConvertError::ParseClause { input, message } => {
118                write!(f, "failed to parse clause(s) {input:?}: {message}")
119            }
120            SpecConvertError::ParseOrder { input, message } => {
121                write!(f, "failed to parse order {input:?}: {message}")
122            }
123            SpecConvertError::LegacyValidate { errors } => {
124                write!(f, "legacy AST validation failed: {}", errors.join("; "))
125            }
126            SpecConvertError::Convert(e) => {
127                write!(f, "algebra conversion failed: {e}")
128            }
129        }
130    }
131}
132
133impl std::error::Error for SpecConvertError {}
134
135impl From<ConvertError> for SpecConvertError {
136    fn from(e: ConvertError) -> Self {
137        SpecConvertError::Convert(e)
138    }
139}
140
141/// Parse a single inline `for`-clause string — **possibly
142/// multi-clause**, e.g. `"k in 1..10, limit in [1, 2, 3]"` — into the
143/// algebra-layer [`AlgebraAst`].
144///
145/// The canonical entry for consumers that hold only the for-clause text
146/// (no separate `where` / `order`): both **scenario- and phase-level**
147/// `for_each` in a host route through this, so the comprehension grammar
148/// has a single owner (polydat) rather than ad-hoc `var in expr` splits
149/// scattered in the runtime.
150pub fn parse_inline(spec: &str) -> Result<AlgebraAst, SpecConvertError> {
151    ComprehensionSpec {
152        r#for: ForSpec::Inline(spec.to_string()),
153        r#where: None,
154        order: None,
155    }
156    .into_algebra()
157}
158
159impl ComprehensionSpec {
160    /// Convert this spec into the algebra-layer
161    /// [`AlgebraAst`].  Routes the `for` shape through the
162    /// clause parser, builds the flat clause form (applying `where`
163    /// / `order` modifiers), and lowers it with
164    /// [`clauses_to_algebra`].
165    pub fn into_algebra(self) -> Result<AlgebraAst, SpecConvertError> {
166        let legacy = self.into_legacy()?;
167        let algebra = clauses_to_algebra(&legacy)?;
168        Ok(algebra)
169    }
170
171    /// Build the intermediate flat clause form. Exposed for tests
172    /// and for a consumer that reads that form.
173    pub fn into_legacy(self) -> Result<ClauseAst, SpecConvertError> {
174        let subspaces = self.r#for.into_subspaces()?;
175        let mut legacy = comprehension_from_subspaces(subspaces);
176        if let Some(predicate) = self.r#where {
177            legacy = legacy.with_filter(predicate);
178        }
179        if let Some(order_text) = self.order {
180            let order =
181                parse_order_spec(&order_text).map_err(|msg| SpecConvertError::ParseOrder {
182                    input: order_text.clone(),
183                    message: msg,
184                })?;
185            legacy = legacy.with_order(order);
186        }
187        legacy
188            .validate()
189            .map_err(|errs| SpecConvertError::LegacyValidate { errors: errs })?;
190        Ok(legacy)
191    }
192}
193
194impl ForSpec {
195    /// Lower a `ForSpec` to the `Vec<Vec<Clause>>` shape that
196    /// [`comprehension_from_subspaces`] expects.
197    fn into_subspaces(self) -> Result<Vec<Vec<LegacyClause>>, SpecConvertError> {
198        match self {
199            ForSpec::Inline(text) => {
200                // One inline string = one sub-space per clause
201                // (matches `parse_comprehension_text`'s
202                // convention so the union-detection rule sees
203                // per-clause boundaries).
204                let clauses =
205                    parse_clause_list(&text).map_err(|message| SpecConvertError::ParseClause {
206                        input: text.clone(),
207                        message,
208                    })?;
209                Ok(clauses.into_iter().map(|c| vec![c]).collect())
210            }
211            ForSpec::ClauseList(entries) => {
212                // Each entry is one clause's text — one
213                // sub-space per entry (same convention).
214                let mut subspaces = Vec::with_capacity(entries.len());
215                for entry in entries {
216                    let clauses = parse_clause_list(&entry).map_err(|message| {
217                        SpecConvertError::ParseClause {
218                            input: entry.clone(),
219                            message,
220                        }
221                    })?;
222                    for c in clauses {
223                        subspaces.push(vec![c]);
224                    }
225                }
226                Ok(subspaces)
227            }
228            ForSpec::UnionOfClauseLists(groups) => {
229                // Each inner list is one sub-space (cartesian
230                // over those clauses).
231                let mut subspaces = Vec::with_capacity(groups.len());
232                for group in groups {
233                    let mut subspace_clauses = Vec::with_capacity(group.len());
234                    for entry in group {
235                        let clauses = parse_clause_list(&entry).map_err(|message| {
236                            SpecConvertError::ParseClause {
237                                input: entry.clone(),
238                                message,
239                            }
240                        })?;
241                        subspace_clauses.extend(clauses);
242                    }
243                    subspaces.push(subspace_clauses);
244                }
245                Ok(subspaces)
246            }
247        }
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254    use crate::comprehension::strategy::StrategyName;
255
256    #[test]
257    fn inline_single_clause() {
258        let yaml = r#"
259            for: "k in 1..10"
260        "#;
261        let spec: ComprehensionSpec = serde_yaml::from_str(yaml).unwrap();
262        let algebra = spec.into_algebra().unwrap();
263        // Single-clause cartesian collapses to the bare Clause.
264        assert!(matches!(algebra, AlgebraAst::Clause { .. }));
265    }
266
267    #[test]
268    fn inline_multi_clause_cartesian() {
269        let yaml = r#"
270            for: "k in 1..10, limit in [10, 100, 1000]"
271        "#;
272        let spec: ComprehensionSpec = serde_yaml::from_str(yaml).unwrap();
273        let algebra = spec.into_algebra().unwrap();
274        match algebra {
275            AlgebraAst::Cartesian { children } => assert_eq!(children.len(), 2),
276            other => panic!("expected Cartesian, got {other:?}"),
277        }
278    }
279
280    #[test]
281    fn clause_list_form_cartesian() {
282        let yaml = r#"
283            for:
284              - "k in 1..10"
285              - "limit in [10, 100, 1000]"
286        "#;
287        let spec: ComprehensionSpec = serde_yaml::from_str(yaml).unwrap();
288        let algebra = spec.into_algebra().unwrap();
289        match algebra {
290            AlgebraAst::Cartesian { children } => assert_eq!(children.len(), 2),
291            other => panic!("expected Cartesian, got {other:?}"),
292        }
293    }
294
295    #[test]
296    fn union_of_clause_lists() {
297        let yaml = r#"
298            for:
299              - ["k in 10",  "limit in [1, 2, 3]"]
300              - ["k in 100", "limit in [10, 20, 30]"]
301        "#;
302        let spec: ComprehensionSpec = serde_yaml::from_str(yaml).unwrap();
303        let algebra = spec.into_algebra().unwrap();
304        match algebra {
305            AlgebraAst::Union { children } => assert_eq!(children.len(), 2),
306            other => panic!("expected Union, got {other:?}"),
307        }
308    }
309
310    #[test]
311    fn where_clause_wraps_with_filter() {
312        let yaml = r#"
313            for: "k in 1..10"
314            where: "{k} > 5"
315        "#;
316        let spec: ComprehensionSpec = serde_yaml::from_str(yaml).unwrap();
317        let algebra = spec.into_algebra().unwrap();
318        assert!(matches!(algebra, AlgebraAst::Filter { .. }));
319    }
320
321    #[test]
322    fn order_clause_wraps_with_order() {
323        let yaml = r#"
324            for: "k in 1..10, limit in 1..100"
325            order: "halton/50"
326        "#;
327        let spec: ComprehensionSpec = serde_yaml::from_str(yaml).unwrap();
328        let algebra = spec.into_algebra().unwrap();
329        match algebra {
330            AlgebraAst::Order {
331                strategy: StrategyName::Halton,
332                truncation: Some(50),
333                ..
334            } => {}
335            other => panic!("expected Order(Halton, Some(50)), got {other:?}"),
336        }
337    }
338
339    #[test]
340    fn where_and_order_compose() {
341        let yaml = r#"
342            for: "k in 1..10, limit in 1..100"
343            where: "{k} * {limit} <= 100"
344            order: "lex/20"
345        "#;
346        let spec: ComprehensionSpec = serde_yaml::from_str(yaml).unwrap();
347        let algebra = spec.into_algebra().unwrap();
348        // Order wraps Filter wraps Cartesian
349        match algebra {
350            AlgebraAst::Order {
351                child,
352                strategy: StrategyName::Lex,
353                truncation: Some(20),
354                ..
355            } => {
356                assert!(matches!(*child, AlgebraAst::Filter { .. }));
357            }
358            other => panic!("expected Order(Lex, Some(20)) wrapping Filter, got {other:?}"),
359        }
360    }
361
362    #[test]
363    fn json_input_round_trips() {
364        let json = r#"
365            {
366                "for": ["k in 1..10", "limit in [10, 100]"],
367                "where": "{k} > 0",
368                "order": "halton/20"
369            }
370        "#;
371        let spec: ComprehensionSpec = serde_json::from_str(json).unwrap();
372        let algebra = spec.into_algebra().unwrap();
373        assert!(matches!(algebra, AlgebraAst::Order { .. }));
374    }
375
376    #[test]
377    fn malformed_clause_surfaces_error() {
378        let yaml = r#"
379            for: "this is not a valid clause"
380        "#;
381        let spec: ComprehensionSpec = serde_yaml::from_str(yaml).unwrap();
382        let err = spec.into_algebra().unwrap_err();
383        assert!(matches!(err, SpecConvertError::ParseClause { .. }));
384    }
385
386    #[test]
387    fn malformed_order_surfaces_error() {
388        let yaml = r#"
389            for: "k in 1..10"
390            order: "(((not valid"
391        "#;
392        let spec: ComprehensionSpec = serde_yaml::from_str(yaml).unwrap();
393        let err = spec.into_algebra().unwrap_err();
394        assert!(matches!(err, SpecConvertError::ParseOrder { .. }));
395    }
396
397    #[test]
398    fn unparseable_source_inside_for_falls_back_to_generator() {
399        // parse_source treats unrecognized text as a
400        // Source::Generator that the runtime evaluator resolves
401        // against the Polydat Kernel chain.
402        let yaml = r#"
403            for: "k in something-weird"
404        "#;
405        let spec: ComprehensionSpec = serde_yaml::from_str(yaml).unwrap();
406        let algebra = spec.into_algebra().expect("permissive accept");
407        match algebra {
408            AlgebraAst::Clause { source, .. } => match source {
409                crate::comprehension::source::Source::Generator { expr, .. } => {
410                    assert_eq!(expr, "something-weird");
411                }
412                other => panic!("expected Generator, got {other:?}"),
413            },
414            other => panic!("expected Clause, got {other:?}"),
415        }
416    }
417}