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}