Skip to content

Commit b820599

Browse files
timfennisclaude
andcommitted
perf(stdlib): make enumerate return a lazy iterator 🦥
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
1 parent 2f4a0df commit b820599

4 files changed

Lines changed: 68 additions & 12 deletions

File tree

‎ndc_stdlib/src/sequence.rs‎

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use ndc_core::compare::FallibleOrd;
55
use ndc_macros::export_module;
66
use ndc_vm::VmCallable;
77
use ndc_vm::value::{Object, SeqValue, Value};
8-
use ndc_vm::{CombinationsIter, TakeIter};
8+
use ndc_vm::{CombinationsIter, EnumerateIter, TakeIter};
99
use std::cmp::Ordering;
1010

1111
fn try_sort_by<E>(
@@ -334,16 +334,12 @@ mod inner {
334334
}
335335
}
336336

337-
/// Enumerates the given sequence returning a list of tuples where the first element of the tuple is the index of the element in the input sequence.
338-
#[function(return_type = Vec<_>)]
337+
/// Returns a lazy iterator yielding `(index, value)` tuples for each element of `seq`.
338+
#[function(return_type = Iterator<Value>)]
339339
pub fn enumerate(seq: SeqValue) -> anyhow::Result<Value> {
340-
Ok(Value::list(
341-
seq.try_into_iter()
342-
.ok_or_else(|| anyhow!("enumerate requires a sequence"))?
343-
.enumerate()
344-
.map(|(i, v)| Value::tuple(vec![Value::Int(i as i64), v]))
345-
.collect(),
346-
))
340+
let iter =
341+
EnumerateIter::new(seq).ok_or_else(|| anyhow!("enumerate requires a sequence"))?;
342+
Ok(Value::iterator(iter.into_shared()))
347343
}
348344

349345
/// Reduces/folds the given sequence using the given combining function and a custom initial value.

‎ndc_vm/src/iterator.rs‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,58 @@ impl VmIterator for TakeIter {
574574
}
575575
}
576576

577+
/// Pairs each upstream element with its 0-based index, lazily.
578+
///
579+
/// `deep_copy` is supported when the source is a `Shared` iterator. It returns
580+
/// `None` for eager sources (list/deque/map `IntoIter`s) since those lack a
581+
/// copy mechanism — same constraint as [`TakeIter`].
582+
pub struct EnumerateIter {
583+
source: ValueIter,
584+
index: usize,
585+
}
586+
587+
impl EnumerateIter {
588+
/// Returns `None` if `value` is not iterable.
589+
pub fn new(value: Value) -> Option<Self> {
590+
Some(Self {
591+
source: value.try_into_iter()?,
592+
index: 0,
593+
})
594+
}
595+
596+
pub fn into_shared(self) -> SharedIterator {
597+
Rc::new(RefCell::new(self))
598+
}
599+
}
600+
601+
impl VmIterator for EnumerateIter {
602+
fn next(&mut self) -> Option<Value> {
603+
let v = self.source.next()?;
604+
let i = self.index;
605+
self.index += 1;
606+
Some(Value::tuple(vec![Value::Int(i as i64), v]))
607+
}
608+
609+
fn size_hint(&self) -> (usize, Option<usize>) {
610+
match &self.source {
611+
ValueIter::Shared(s) => s.borrow().size_hint(),
612+
ValueIter::List(it) => it.size_hint(),
613+
ValueIter::Deque(it) => it.size_hint(),
614+
ValueIter::Map(it) => it.size_hint(),
615+
}
616+
}
617+
618+
fn deep_copy(&self) -> Option<SharedIterator> {
619+
let ValueIter::Shared(shared) = &self.source else {
620+
return None;
621+
};
622+
Some(Rc::new(RefCell::new(Self {
623+
source: ValueIter::Shared(shared.borrow().deep_copy()?),
624+
index: self.index,
625+
})))
626+
}
627+
}
628+
577629
/// Iterates over string characters, yielding each as a single-char string
578630
pub struct StringIter {
579631
string: Rc<RefCell<String>>,

‎ndc_vm/src/lib.rs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ pub use vm::*;
1313

1414
pub use compiler::CompileError;
1515
pub use error::VmError;
16-
pub use iterator::{CombinationsIter, RepeatIter, SharedIterator, TakeIter, VmIterator};
16+
pub use iterator::{
17+
CombinationsIter, EnumerateIter, RepeatIter, SharedIterator, TakeIter, VmIterator,
18+
};
1719
pub use value::*;
1820

1921
#[cfg(test)]
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
let my_list = ["Foo", "Bar", "Baz"];
2-
let out = my_list.enumerate();
2+
let out = my_list.enumerate().list();
33

44
assert_eq(out[0], (0, "Foo"));
55
assert_eq(out[1], (1, "Bar"));
66
assert_eq(out[2], (2, "Baz"));
7+
8+
// Laziness: enumerating an unbounded range and taking is finite.
9+
assert_eq((0..).enumerate().take(3).list(), [(0, 0), (1, 1), (2, 2)]);
10+
11+
// Enumeration composes with iterator combinators.
12+
assert_eq([10, 20, 30].enumerate().map(fn(p) { p[0] + p[1] }), [10, 21, 32]);

0 commit comments

Comments
 (0)