Skip to main content

polydat_core/iteration/comprehension/
source_values.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! The runtime's reading of a value as a comprehension source: which
5//! values peel into elements, and how a string source strips into
6//! typed tokens. The grammar's separator rule
7//! (`source::split_string_comprehension`) is the one both sides use.
8
9use super::source::split_string_comprehension;
10
11/// The canonical "is this value peelable, and into what?" predicate
12/// (comprehension_forms.md §3.1.2). Returns `Some(interior)` when `v` has an iteration
13/// interior — peeling it one level yields these elements — and
14/// `None` when `v` is an iteration scalar (relaxed wraps it; an
15/// explicit `[v…]` destructure errors).
16///
17/// This is the single place the peel/wrap decision is made,
18/// replacing the scattered per-type special cases (the
19/// `as_partition_list()`-peel / `Ok(other)`-wrap arms) that used
20/// to live in `eval::evaluate_spec_internal`.
21///
22/// `Value::Str` is iterable — its interior is its
23/// **string-comprehension tokens** ([`strip_string_tokens`]). The
24/// single-quoted *atomic* form is resolved earlier, at the
25/// source-text layer (the parser produces a one-element literal),
26/// so by the time a bare `Value::Str` reaches this predicate the
27/// intent is "iterate it." A whole-string binding is reached via
28/// the no-peel form `x in [s]`.
29pub fn iteration_interior(v: &crate::ast::Value) -> Option<Vec<crate::ast::Value>> {
30    use crate::ast::Value;
31    match v {
32        Value::VecF32(s) => Some(s.as_slice().iter().map(|x| Value::F64(*x as f64)).collect()),
33        Value::VecF64(s) => Some(s.as_slice().iter().map(|x| Value::F64(*x)).collect()),
34        Value::VecF16(s) => Some(
35            s.as_slice()
36                .iter()
37                .map(|x| Value::F64(x.to_f64()))
38                .collect(),
39        ),
40        // Signed lanes peel to the honest signed carrier — a
41        // VecI64 holding -5 iterates as I64(-5), not the unsigned
42        // bit-reinterpretation (type_system_alignment.md §5).
43        Value::VecI32(s) => Some(s.as_slice().iter().map(|x| Value::I64(*x as i64)).collect()),
44        Value::VecI64(s) => Some(s.as_slice().iter().map(|x| Value::I64(*x)).collect()),
45        Value::VecI16(s) => Some(s.as_slice().iter().map(|x| Value::I64(*x as i64)).collect()),
46        Value::VecI8(s) => Some(s.as_slice().iter().map(|x| Value::I64(*x as i64)).collect()),
47        // A JSON array peels to its elements (each carried as a
48        // Json value); a non-array JSON is an iteration scalar.
49        Value::Json(j) => j.as_array().map(|arr| {
50            arr.iter()
51                .map(|e| Value::Json(std::sync::Arc::new(e.clone())))
52                .collect()
53        }),
54        // Ext carrying a PartitionList peels into its partitions
55        // (cursor_partitions.md §7.1). Other Ext values are opaque
56        // scalars.
57        Value::Ext(_) => v.as_partition_list().map(|list| {
58            list.as_slice()
59                .iter()
60                .map(|p| Value::from_partition(*p))
61                .collect()
62        }),
63        // Lane-typed register views peel like the Vec* family
64        // (a reg_f32x4 is a fixed-width typed vector); the Raw
65        // view is an opaque buffer-state word — an iteration
66        // scalar.
67        Value::Reg128(b, view) => {
68            use crate::ast::RegLanes;
69            match view {
70                RegLanes::Raw => None,
71                RegLanes::I8x16 => {
72                    Some(b.lanes_i8().iter().map(|x| Value::I64(*x as i64)).collect())
73                }
74                RegLanes::I16x8 => Some(
75                    b.lanes_i16()
76                        .iter()
77                        .map(|x| Value::I64(*x as i64))
78                        .collect(),
79                ),
80                RegLanes::I32x4 => Some(
81                    b.lanes_i32()
82                        .iter()
83                        .map(|x| Value::I64(*x as i64))
84                        .collect(),
85                ),
86                RegLanes::I64x2 => Some(b.lanes_i64().iter().map(|x| Value::I64(*x)).collect()),
87                RegLanes::F16x8 => Some(
88                    b.lanes_f16()
89                        .iter()
90                        .map(|x| Value::F64(x.to_f64()))
91                        .collect(),
92                ),
93                RegLanes::F32x4 => Some(
94                    b.lanes_f32()
95                        .iter()
96                        .map(|x| Value::F64(*x as f64))
97                        .collect(),
98                ),
99                RegLanes::F64x2 => Some(b.lanes_f64().iter().map(|x| Value::F64(*x)).collect()),
100            }
101        }
102        // A string's interior is its comprehension tokens.
103        Value::Str(s) => Some(strip_string_tokens(s)),
104        // Iteration scalars — relaxed wraps, `[v…]` errors.
105        Value::U64(_)
106        | Value::I64(_)
107        | Value::U128(_)
108        | Value::I128(_)
109        | Value::F64(_)
110        | Value::Bool(_)
111        | Value::Bytes(_)
112        | Value::Handle(_)
113        | Value::None => None,
114    }
115}
116
117/// Split a string-comprehension source into its token values
118/// (comprehension_forms.md §3.1.3). Separators are runs of comma, semicolon, and
119/// ASCII whitespace; every other character — notably `:` (k:v /
120/// `a:b:c` tuples), `.` (floats), `-` (negatives / hyphenated
121/// labels), `/` — stays inside the token. Each token is typed
122/// like a literal-list element (u64 / f64 / bool / else Str), so
123/// `"1, 2, 3"` yields numeric values and `"a, b"` yields strings.
124///
125/// A single-token string (`"OTHER"`) yields a one-element vec, so
126/// the single-value case degenerates to the no-peel binding for
127/// free.
128pub fn strip_string_tokens(s: &str) -> Vec<crate::ast::Value> {
129    use crate::ast::Value;
130    split_string_comprehension(s)
131        .into_iter()
132        .map(|t| {
133            if let Ok(n) = t.parse::<u64>() {
134                Value::U64(n)
135            } else if let Ok(f) = t.parse::<f64>() {
136                Value::F64(f)
137            } else if t == "true" {
138                Value::Bool(true)
139            } else if t == "false" {
140                Value::Bool(false)
141            } else {
142                Value::Str(t.to_string().into())
143            }
144        })
145        .collect()
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use crate::ast::Value;
152
153    #[test]
154    fn string_strips_on_comma_semicolon_whitespace_retaining_colons() {
155        // comprehension_forms.md §3.1.3: separators are comma /
156        // semicolon / ws;
157        // colons (and dots, dashes) stay in-token.
158        let got = strip_string_tokens("a:1, b:2; c:3 d:4");
159        assert_eq!(
160            got,
161            vec![
162                Value::Str("a:1".into()),
163                Value::Str("b:2".into()),
164                Value::Str("c:3".into()),
165                Value::Str("d:4".into()),
166            ]
167        );
168    }
169
170    #[test]
171    fn string_tokens_are_typed_like_literals() {
172        // Floats survive (dot retained), ints type as U64.
173        assert_eq!(
174            strip_string_tokens("1, 2, 3"),
175            vec![Value::U64(1), Value::U64(2), Value::U64(3)]
176        );
177        assert_eq!(
178            strip_string_tokens("1.5, 2.5"),
179            vec![Value::F64(1.5), Value::F64(2.5)]
180        );
181    }
182
183    #[test]
184    fn single_token_string_degenerates_to_singleton() {
185        assert_eq!(
186            strip_string_tokens("OTHER"),
187            vec![Value::Str("OTHER".into())]
188        );
189    }
190
191    #[test]
192    fn iteration_interior_string_is_its_tokens() {
193        let v = Value::Str("x, y, z".into());
194        assert_eq!(
195            iteration_interior(&v),
196            Some(vec![
197                Value::Str("x".into()),
198                Value::Str("y".into()),
199                Value::Str("z".into())
200            ])
201        );
202    }
203
204    #[test]
205    fn iteration_interior_vector_peels_to_elements() {
206        let v = Value::VecI32(crate::ast::SliceArc::from_vec(vec![10, 20, 30]));
207        assert_eq!(
208            iteration_interior(&v),
209            Some(vec![Value::I64(10), Value::I64(20), Value::I64(30)])
210        );
211    }
212
213    #[test]
214    fn iteration_interior_signed_vector_peels_signed() {
215        // The honest-I64 alignment fix: negative lane elements
216        // iterate as their signed values, not the unsigned
217        // bit-reinterpretation (type_system_alignment.md §5).
218        let v = Value::VecI64(crate::ast::SliceArc::from_vec(vec![-5_i64, 7]));
219        assert_eq!(
220            iteration_interior(&v),
221            Some(vec![Value::I64(-5), Value::I64(7)])
222        );
223    }
224
225    #[test]
226    fn iteration_interior_scalars_are_none() {
227        assert_eq!(iteration_interior(&Value::U64(5)), None);
228        assert_eq!(iteration_interior(&Value::F64(1.5)), None);
229        assert_eq!(iteration_interior(&Value::Bool(true)), None);
230    }
231}