Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions benches/programs/enumerate_find.ndc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Early-exit search: lazy enumerate avoids allocating tuples
// for elements past the target.
let xs = list(0..1_000_000);
let target = 750_000;
let found = xs.enumerate().find(fn(p) => p[1] == target);
print(found);
8 changes: 8 additions & 0 deletions benches/programs/enumerate_for_loop.ndc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Common pattern: iterate every element with its index.
// Lazy version avoids allocating an intermediate list of tuples.
let xs = list(0..500_000);
let acc = 0;
for (i, v) in xs.enumerate() {
acc += i + v;
}
print(acc);
5 changes: 5 additions & 0 deletions benches/programs/enumerate_take_small.ndc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Lazy enumerate should win big: only the first 10 tuples are needed,
// but the eager version allocates a full Vec of 1_000_000 tuples first.
let xs = list(0..1_000_000);
let head = xs.enumerate().take(10).list();
print(head.len);
6 changes: 6 additions & 0 deletions benches/programs/enumerate_to_list.ndc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Worst case for lazy: full materialization to a list.
// Lazy adds one indirection layer per element; expect a small regression
// or near-parity here.
let xs = list(0..500_000);
let result = xs.enumerate().list();
print(result.len);
16 changes: 6 additions & 10 deletions ndc_stdlib/src/sequence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use ndc_core::compare::FallibleOrd;
use ndc_macros::export_module;
use ndc_vm::VmCallable;
use ndc_vm::value::{Object, SeqValue, Value};
use ndc_vm::{CombinationsIter, TakeIter};
use ndc_vm::{CombinationsIter, EnumerateIter, TakeIter};
use std::cmp::Ordering;

fn try_sort_by<E>(
Expand Down Expand Up @@ -334,16 +334,12 @@ mod inner {
}
}

/// 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.
#[function(return_type = Vec<_>)]
/// Returns a lazy iterator yielding `(index, value)` tuples for each element of `seq`.
#[function(return_type = Iterator<Value>)]
pub fn enumerate(seq: SeqValue) -> anyhow::Result<Value> {
Ok(Value::list(
seq.try_into_iter()
.ok_or_else(|| anyhow!("enumerate requires a sequence"))?
.enumerate()
.map(|(i, v)| Value::tuple(vec![Value::Int(i as i64), v]))
.collect(),
))
let iter =
EnumerateIter::new(seq).ok_or_else(|| anyhow!("enumerate requires a sequence"))?;
Ok(Value::iterator(iter.into_shared()))
}

/// Reduces/folds the given sequence using the given combining function and a custom initial value.
Expand Down
52 changes: 52 additions & 0 deletions ndc_vm/src/iterator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,58 @@ impl VmIterator for TakeIter {
}
}

/// Pairs each upstream element with its 0-based index, lazily.
///
/// `deep_copy` is supported when the source is a `Shared` iterator. It returns
/// `None` for eager sources (list/deque/map `IntoIter`s) since those lack a
/// copy mechanism — same constraint as [`TakeIter`].
pub struct EnumerateIter {
source: ValueIter,
index: usize,
}

impl EnumerateIter {
/// Returns `None` if `value` is not iterable.
pub fn new(value: Value) -> Option<Self> {
Some(Self {
source: value.try_into_iter()?,
index: 0,
})
}

pub fn into_shared(self) -> SharedIterator {
Rc::new(RefCell::new(self))
}
}

impl VmIterator for EnumerateIter {
fn next(&mut self) -> Option<Value> {
let v = self.source.next()?;
let i = self.index;
self.index += 1;
Some(Value::tuple(vec![Value::Int(i as i64), v]))
}

fn size_hint(&self) -> (usize, Option<usize>) {
match &self.source {
ValueIter::Shared(s) => s.borrow().size_hint(),
ValueIter::List(it) => it.size_hint(),
ValueIter::Deque(it) => it.size_hint(),
ValueIter::Map(it) => it.size_hint(),
}
}

fn deep_copy(&self) -> Option<SharedIterator> {
let ValueIter::Shared(shared) = &self.source else {
return None;
};
Some(Rc::new(RefCell::new(Self {
source: ValueIter::Shared(shared.borrow().deep_copy()?),
index: self.index,
})))
}
}

/// Iterates over string characters, yielding each as a single-char string
pub struct StringIter {
string: Rc<RefCell<String>>,
Expand Down
4 changes: 3 additions & 1 deletion ndc_vm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ pub use vm::*;

pub use compiler::CompileError;
pub use error::VmError;
pub use iterator::{CombinationsIter, RepeatIter, SharedIterator, TakeIter, VmIterator};
pub use iterator::{
CombinationsIter, EnumerateIter, RepeatIter, SharedIterator, TakeIter, VmIterator,
};
pub use value::*;

#[cfg(test)]
Expand Down
8 changes: 7 additions & 1 deletion tests/functional/programs/603_stdlib_seq/002_enumerate.ndc
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
let my_list = ["Foo", "Bar", "Baz"];
let out = my_list.enumerate();
let out = my_list.enumerate().list();

assert_eq(out[0], (0, "Foo"));
assert_eq(out[1], (1, "Bar"));
assert_eq(out[2], (2, "Baz"));

// Laziness: enumerating an unbounded range and taking is finite.
assert_eq((0..).enumerate().take(3).list(), [(0, 0), (1, 1), (2, 2)]);

// Enumeration composes with iterator combinators.
assert_eq([10, 20, 30].enumerate().map(fn(p) { p[0] + p[1] }), [10, 21, 32]);
Loading