brink_runtime/iter.rs
1//! NS-A3 (issue #1109, docs/stdlib-spec.md §9.6): the iterate protocol's
2//! pull-shaped machine form over the CLOSED builtin iterable set.
3//!
4//! `iterate` is the registry's third entry — `next(ref Self): Option[T]`,
5//! row ⊆ writes-receiver·silent·total, with laws attached: **every element
6//! exactly once; `none` is terminal and sticky** (property-harness
7//! enforced, `tests/law_iterate.rs`). Pull-shaped was RULED over
8//! push/`each` because a push-desugared `for` body would be an fn-value
9//! callback and functions never await — pull desugars inline and iterators
10//! park across suspensions for free.
11//!
12//! v1 wires the protocol machinery over the builtin iterables only —
13//! arrays (values) and maps (keys, insertion order, key set snapshotted at
14//! iteration start per F10) — and `for` is the only consumer. The `for`
15//! desugar itself compiles to an index walk over the same canonical
16//! sequence (`CollectionKeys` + `IndexGet`; `brink-ir`'s
17//! `lir::lower::blocks`), **not** to calls through this type: the desugar
18//! is observably unchanged, and [`ValueIter`] is that same iteration
19//! contract reified as a value. Both share one snapshot function
20//! ([`crate::collection_ops::iteration_sequence`]) so they can never
21//! drift; the law harness pins their agreement. This is the seam later
22//! waves consume: A5's range iterators park in `FlowFrame` spills as
23//! durable values, B2's `for k, v` desugar reads the same sequence, and
24//! user-type impls join behind #1090.
25
26use alloc::sync::Arc;
27use alloc::vec::Vec;
28
29use brink_format::Value;
30
31use crate::collection_ops::iteration_sequence;
32use crate::error::RuntimeError;
33
34/// A pull iterator over one builtin iterable's canonical sequence — the
35/// iterate protocol's machine form: [`next`](Self::next) is
36/// `next(ref Self): Option[T]` with the protocol's laws structural (the
37/// cursor only advances, so every element is yielded exactly once and
38/// exhaustion is terminal and sticky by construction).
39///
40/// Holds the snapshot the protocol's F10 ruling requires: constructing the
41/// iterator snapshots a map's key set eagerly (arrays share their storage
42/// — COW makes the snapshot free), so structural modification of the
43/// source collection mid-iteration is invisible to an already-created
44/// iterator.
45#[derive(Debug, Clone)]
46pub struct ValueIter {
47 seq: IterSeq,
48 idx: u64,
49}
50
51/// The snapshot behind a [`ValueIter`]: a materialized sequence for
52/// arrays/maps, or the range bounds themselves (NS-A5, F7) — a range IS
53/// its own canonical sequence, so iterating `0..1_000_000` never
54/// materializes a million-element vector, mirroring the `for` desugar's
55/// `CollectionKeys` identity pass-through for ranges.
56#[derive(Debug, Clone)]
57enum IterSeq {
58 Seq(Arc<Vec<Value>>),
59 Range { start: i64, len: u64 },
60}
61
62impl ValueIter {
63 /// Snapshot `iterable`'s canonical sequence (arrays: values; maps:
64 /// keys in insertion order; ranges: their own bounds — O(1)). Faults
65 /// `NotIndexable` for anything outside the closed builtin iterable
66 /// set — the same fault the `for` desugar's `CollectionKeys` raises.
67 pub fn new(iterable: &Value) -> Result<Self, RuntimeError> {
68 let seq = match (iterable.as_range(), iterable.range_len()) {
69 (Some((start, _, _)), Some(len)) => IterSeq::Range {
70 start: i64::from(start),
71 #[expect(clippy::cast_sign_loss, reason = "range_len is never negative")]
72 len: len as u64,
73 },
74 _ => IterSeq::Seq(iteration_sequence(iterable.clone())?),
75 };
76 Ok(Self { seq, idx: 0 })
77 }
78
79 /// How many elements remain to be pulled (saturated at `usize::MAX`
80 /// for the degenerate 2³¹+-element ranges on 32-bit targets).
81 #[must_use]
82 pub fn remaining(&self) -> usize {
83 let total = match &self.seq {
84 IterSeq::Seq(seq) => seq.len() as u64,
85 IterSeq::Range { len, .. } => *len,
86 };
87 usize::try_from(total.saturating_sub(self.idx)).unwrap_or(usize::MAX)
88 }
89}
90
91/// The pull shape itself — `next(&mut self): Option<Value>` IS the
92/// protocol method's machine form, so it lives on the standard trait:
93/// `Some(element)` until the sequence is exhausted, then `None` forever
94/// (terminal and sticky — the cursor never rewinds, making the fused-
95/// iterator law structural).
96impl Iterator for ValueIter {
97 type Item = Value;
98
99 fn next(&mut self) -> Option<Value> {
100 let item = match &self.seq {
101 IterSeq::Seq(seq) => seq.get(usize::try_from(self.idx).ok()?).cloned()?,
102 IterSeq::Range { start, len } => {
103 if self.idx >= *len {
104 return None;
105 }
106 #[expect(
107 clippy::cast_possible_wrap,
108 clippy::cast_possible_truncation,
109 reason = "start + idx is an element of the range by construction, so it fits i32"
110 )]
111 Value::Int((start + self.idx as i64) as i32)
112 }
113 };
114 self.idx += 1;
115 Some(item)
116 }
117
118 fn size_hint(&self) -> (usize, Option<usize>) {
119 let n = self.remaining();
120 (n, Some(n))
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use brink_format::OrderedMap;
127
128 use super::*;
129
130 #[test]
131 fn range_iterates_elements_in_order_without_materializing() {
132 // NS-A5 (F7): ranges join the closed iterable set. Exclusive form.
133 let r = Value::range(2, 5, false);
134 let it = ValueIter::new(&r).unwrap();
135 assert_eq!(it.remaining(), 3);
136 let got: Vec<Value> = it.collect();
137 assert_eq!(got, vec![Value::Int(2), Value::Int(3), Value::Int(4)]);
138 // Inclusive form.
139 let got: Vec<Value> = ValueIter::new(&Value::range(-1, 1, true))
140 .unwrap()
141 .collect();
142 assert_eq!(got, vec![Value::Int(-1), Value::Int(0), Value::Int(1)]);
143 // Empty ranges iterate zero times — emptiness is load-bearing.
144 for r in [Value::range(0, 0, false), Value::range(5, 2, true)] {
145 let mut it = ValueIter::new(&r).unwrap();
146 assert_eq!(it.remaining(), 0);
147 assert_eq!(it.next(), None);
148 // Terminal and sticky.
149 assert_eq!(it.next(), None);
150 }
151 }
152
153 #[test]
154 fn array_iterates_values_in_order() {
155 let a = Value::array(vec![Value::Int(3), Value::Int(1), Value::Int(2)]);
156 let mut it = ValueIter::new(&a).unwrap();
157 assert_eq!(it.remaining(), 3);
158 assert_eq!(it.next(), Some(Value::Int(3)));
159 assert_eq!(it.next(), Some(Value::Int(1)));
160 assert_eq!(it.next(), Some(Value::Int(2)));
161 assert_eq!(it.next(), None);
162 }
163
164 #[test]
165 fn map_iterates_keys_in_insertion_order() {
166 let mut m = OrderedMap::new();
167 m.insert(brink_format::MapKey::Str("z".into()), Value::Int(1));
168 m.insert(brink_format::MapKey::Str("a".into()), Value::Int(2));
169 m.insert(brink_format::MapKey::Int(7), Value::Int(3));
170 let mut it = ValueIter::new(&Value::map(m)).unwrap();
171 assert_eq!(it.next(), Some(Value::String("z".into())));
172 assert_eq!(it.next(), Some(Value::String("a".into())));
173 assert_eq!(it.next(), Some(Value::Int(7)));
174 assert_eq!(it.next(), None);
175 }
176
177 #[test]
178 fn none_is_terminal_and_sticky() {
179 let a = Value::array(vec![Value::Int(1)]);
180 let mut it = ValueIter::new(&a).unwrap();
181 assert_eq!(it.next(), Some(Value::Int(1)));
182 for _ in 0..16 {
183 assert_eq!(it.next(), None);
184 }
185 assert_eq!(it.remaining(), 0);
186 }
187
188 #[test]
189 fn non_iterable_faults_not_indexable() {
190 let err = ValueIter::new(&Value::Int(3)).unwrap_err();
191 assert!(matches!(err, RuntimeError::NotIndexable("int")), "{err:?}");
192 }
193
194 #[test]
195 fn snapshot_is_immune_to_source_mutation() {
196 // F10: the sequence snapshots at iterator creation — COW means a
197 // later mutation of the source collection clones away from the
198 // iterator's Arc, never through it.
199 let mut a = Value::array(vec![Value::Int(1), Value::Int(2)]);
200 let mut it = ValueIter::new(&a).unwrap();
201 if let Some(items) = a.array_make_mut() {
202 items.push(Value::Int(3));
203 }
204 assert_eq!(it.next(), Some(Value::Int(1)));
205 assert_eq!(it.next(), Some(Value::Int(2)));
206 assert_eq!(it.next(), None);
207 }
208}