Skip to main content

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}