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/// (SRD-18f §4). 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        // (SRD-71). Other Ext values are opaque scalars.
56        Value::Ext(_) => v.as_partition_list().map(|list| {
57            list.as_slice()
58                .iter()
59                .map(|p| Value::from_partition(*p))
60                .collect()
61        }),
62        // Lane-typed register views peel like the Vec* family
63        // (a reg_f32x4 is a fixed-width typed vector); the Raw
64        // view is an opaque buffer-state word — an iteration
65        // scalar.
66        Value::Reg128(b, view) => {
67            use crate::ast::RegLanes;
68            match view {
69                RegLanes::Raw => None,
70                RegLanes::I8x16 => {
71                    Some(b.lanes_i8().iter().map(|x| Value::I64(*x as i64)).collect())
72                }
73                RegLanes::I16x8 => Some(
74                    b.lanes_i16()
75                        .iter()
76                        .map(|x| Value::I64(*x as i64))
77                        .collect(),
78                ),
79                RegLanes::I32x4 => Some(
80                    b.lanes_i32()
81                        .iter()
82                        .map(|x| Value::I64(*x as i64))
83                        .collect(),
84                ),
85                RegLanes::I64x2 => Some(b.lanes_i64().iter().map(|x| Value::I64(*x)).collect()),
86                RegLanes::F16x8 => Some(
87                    b.lanes_f16()
88                        .iter()
89                        .map(|x| Value::F64(x.to_f64()))
90                        .collect(),
91                ),
92                RegLanes::F32x4 => Some(
93                    b.lanes_f32()
94                        .iter()
95                        .map(|x| Value::F64(*x as f64))
96                        .collect(),
97                ),
98                RegLanes::F64x2 => Some(b.lanes_f64().iter().map(|x| Value::F64(*x)).collect()),
99            }
100        }
101        // A string's interior is its comprehension tokens.
102        Value::Str(s) => Some(strip_string_tokens(s)),
103        // Iteration scalars — relaxed wraps, `[v…]` errors.
104        Value::U64(_)
105        | Value::I64(_)
106        | Value::U128(_)
107        | Value::I128(_)
108        | Value::F64(_)
109        | Value::Bool(_)
110        | Value::Bytes(_)
111        | Value::Handle(_)
112        | Value::None => None,
113    }
114}
115
116/// Split a string-comprehension source into its token values
117/// (SRD-18f §3.2). Separators are runs of comma, semicolon, and
118/// ASCII whitespace; every other character — notably `:` (k:v /
119/// `a:b:c` tuples), `.` (floats), `-` (negatives / hyphenated
120/// labels), `/` — stays inside the token. Each token is typed
121/// like a literal-list element (u64 / f64 / bool / else Str), so
122/// `"1, 2, 3"` yields numeric values and `"a, b"` yields strings.
123///
124/// A single-token string (`"OTHER"`) yields a one-element vec, so
125/// the single-value case degenerates to the no-peel binding for
126/// free.
127pub fn strip_string_tokens(s: &str) -> Vec<crate::ast::Value> {
128    use crate::ast::Value;
129    split_string_comprehension(s)
130        .into_iter()
131        .map(|t| {
132            if let Ok(n) = t.parse::<u64>() {
133                Value::U64(n)
134            } else if let Ok(f) = t.parse::<f64>() {
135                Value::F64(f)
136            } else if t == "true" {
137                Value::Bool(true)
138            } else if t == "false" {
139                Value::Bool(false)
140            } else {
141                Value::Str(t.to_string().into())
142            }
143        })
144        .collect()
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use crate::ast::Value;
151
152    #[test]
153    fn string_strips_on_comma_semicolon_whitespace_retaining_colons() {
154        // SRD-18f §3.2: separators are comma / semicolon / ws;
155        // colons (and dots, dashes) stay in-token.
156        let got = strip_string_tokens("a:1, b:2; c:3 d:4");
157        assert_eq!(
158            got,
159            vec![
160                Value::Str("a:1".into()),
161                Value::Str("b:2".into()),
162                Value::Str("c:3".into()),
163                Value::Str("d:4".into()),
164            ]
165        );
166    }
167
168    #[test]
169    fn string_tokens_are_typed_like_literals() {
170        // Floats survive (dot retained), ints type as U64.
171        assert_eq!(
172            strip_string_tokens("1, 2, 3"),
173            vec![Value::U64(1), Value::U64(2), Value::U64(3)]
174        );
175        assert_eq!(
176            strip_string_tokens("1.5, 2.5"),
177            vec![Value::F64(1.5), Value::F64(2.5)]
178        );
179    }
180
181    #[test]
182    fn single_token_string_degenerates_to_singleton() {
183        assert_eq!(
184            strip_string_tokens("OTHER"),
185            vec![Value::Str("OTHER".into())]
186        );
187    }
188
189    #[test]
190    fn iteration_interior_string_is_its_tokens() {
191        let v = Value::Str("x, y, z".into());
192        assert_eq!(
193            iteration_interior(&v),
194            Some(vec![
195                Value::Str("x".into()),
196                Value::Str("y".into()),
197                Value::Str("z".into())
198            ])
199        );
200    }
201
202    #[test]
203    fn iteration_interior_vector_peels_to_elements() {
204        let v = Value::VecI32(crate::ast::SliceArc::from_vec(vec![10, 20, 30]));
205        assert_eq!(
206            iteration_interior(&v),
207            Some(vec![Value::I64(10), Value::I64(20), Value::I64(30)])
208        );
209    }
210
211    #[test]
212    fn iteration_interior_signed_vector_peels_signed() {
213        // The honest-I64 alignment fix: negative lane elements
214        // iterate as their signed values, not the unsigned
215        // bit-reinterpretation (type_system_alignment.md §5).
216        let v = Value::VecI64(crate::ast::SliceArc::from_vec(vec![-5_i64, 7]));
217        assert_eq!(
218            iteration_interior(&v),
219            Some(vec![Value::I64(-5), Value::I64(7)])
220        );
221    }
222
223    #[test]
224    fn iteration_interior_scalars_are_none() {
225        assert_eq!(iteration_interior(&Value::U64(5)), None);
226        assert_eq!(iteration_interior(&Value::F64(1.5)), None);
227        assert_eq!(iteration_interior(&Value::Bool(true)), None);
228    }
229}