ADR-212: A Sequence Says What It Is as a Whole — a Range Replays, a Pipeline Walks From Its Back End, and Every Taking Is Seen

Status: Superseded by ADR-293 — its D1-D5 are D16-D20 there Date: September 25, 2026 Target Version: Nikaia 0.0.188 Answers: feedback the owner brought in from outside and then decided: the repairs first — a range in a name and a sequence walked twice reach rustc, which breaks Part III C.1 — and reversing a pipeline must not force a collect() into a list. The owner’s decision: build the repairs, a range’s rev and step_by, and the words a pipeline needs, in one step; leave the two-ended cursor out on purpose (§4) Refines: ADR-105 D1 (Seq[T]’s words) and D2 (the once-only rule, NK2702); ADR-137 D4 (a range is an ordinary expression: it may be given a name, which it could not be walked under) Related: ADR-172 D1 (the step’s three states, beside which these three stand), ADR-094 D4 (a for lends), ADR-048 D1 (no usize a program meets), ADR-054 D2 (a count converted where it is handed over)

1. Context

Four programs, each a pre-alpha tester’s first hour, and where each ended:

  program before
a let r = 0..<3 then for i in r { … } rustc: no method named iter found for Range
b let ks = m.keys() then for k in ks { … } rustc: the same, for Keys
c let s = xs.iter().map(…), then let t = s, then s.count() rustc: use of moved value
d the latest three moving averages of a list windows, rev, take did not exist; map handed back Seq[?]

(a) is the sharpest: Part I 6.x has said since ADR-137 that a range may be given a name, and the checker had no type for one at all. (c) is NK2702’s own subject — a sequence is walked once — seen only where a for or a method walked it, never where a let, an argument, a loop or a lambda took it.

The feedback that raised (d) asked for more than a rev on containers, and it was right in two of its three arguments. A range is not a container, and (0..<n).rev() should not lay one out. And a pipeline — xs.iter().map(f) — is a sequence, so a rev that only containers had would make every reversal of a pipeline a collect() first: the heap copy the language exists to avoid. The third argument, a stream consumed from both ends at once, is §4.

2. What the language below knows that this language did not

Rust answers can this be walked from the back? with a trait, DoubleEndedIterator, and is its length known? with another, ExactSizeIterator, and several adapters need both: take, zip, skip and step_by walked from the back have to know where the back is. A map or a filter needs only the first. So a single word — “reversible” — is wrong in one direction or the other: it either refuses chars().rev() (no length) or allows lines.filter(f).zip(ys).rev() (no length where one is needed), and either way rustc answers about a file nobody wrote.

And one fact the language below gets wrong for this language’s integer: Rust’s Range<i64> has no ExactSizeIterator — its length may not fit a usize on a 32-bit target — so (0..n).step_by(2).rev() does not compile there for n: i64, which is every n a program has.

3. Decisions

D1 — Three words after Seq[T] say what the sequence is as a whole.

Beside the step’s sync, pauses and throws:

Like every word on a Seq, none is in a program’s type grammar (ADR-105 D4): the ledger names them and the checker reads them, and a message never says them (D2).

D2 — On a receiver they are a demand; on a result they pass through.

Seq::rev writes its receiver Seq[$T] ends. A sequence that lacks the word is refused with NK2703 — rev walks a sequence from the back, and this one can only be walked from the front — in the program’s words, with the way out (collect() it, and walk the list from the back). io::lines().rev() is the case it was written for.

A result’s words pass through: the result has a word where the entry writes it and every sequence handed in has it too. So map writes ends sized and a map over io::lines() has neither, while a map over a list’s iter() has both. One ledger column, ends_by_length = true, says the result can be walked from the back only where every input’s length is known — take, zip, skip, step_by — which is §2’s second trait, said once per entry rather than guessed per adapter.

D3 — A range is a sequence that replays, and below it is a value.

a..<b and a..b are Seq[T] sync ends sized replays, with T whichever end has a type. NK2702 never applies to one: walking a range walks a copy of two numbers.

Below, a range that is kept, handed on or called on is nikaia_std::range::Span (..<) or Through (..): Copy, its own iterator, with a length for every integer type, so every walk takes a copy and rustc agrees that r is still there. It slices through index::at as a written range does. A range written straight into a for or into brackets stays Rust’s a..b: it is walked once, or it is a slice, and needs nothing more.

D4 — Every place a sequence is taken is seen, and a named one is walked by value.

Every read of a name that holds a sequence takes it: a let that names it again, an argument, a return, a field, and every method, since a sequence has none that looks without walking. Two reads do not take: an assignment’s target (s = xs.iter() gives it a new one) and a &. NK2702 then refuses the second use, as it did for a for.

And the one the statement order cannot see: a sequence declared outside a loop and taken inside it is gone on the next turn; one taken inside a lambda is gone on the next call, since what the lambda is handed to may run it more than once. Both are NK2702 where they are taken — unless the loop gives the name a new sequence in its body or may leave (break, return) on the turn that took it, which is over-approximated in the direction that refuses less.

Two arms of one choice are not one after the other. Ordered by statement alone, a sequence taken in the then of an if and read in its else was refused — a correct program — and once a list’s iter() is a sequence that would have been every such program. Each take and each read now carries which arm of which if or match it stands in, and a read in another arm of the same choice is not after the take; a read after the whole choice is.

A for over a name that holds a sequence is handed the sequence rather than lent it: no .iter() below, and the binding is each element itself.

D5 — The pipeline, none of it allocating.

map keeps its element type: fn($T) -> $U binds $U from what the lambda’s body comes to — a fact about a block the checker has walked, as spawn’s is, and not a type written for the lambda (ADR-029 D1 is about writing one). Where the parameter’s type states a result, that stands.

New entries: Seq::rev, Seq::zip, Seq::take, Seq::skip, Seq::step_by, and on a list Vec::iter (where a pipeline over a list starts, lent), Vec::windows and Vec::chunks (runs that are views, not copies). Each is the language below’s own adapter, so a chain of them is one loop. A count — take, skip, step_by, nth, chunks, windows — is an i64 here and a usize below; the checker names those arguments by the entry the call resolved to and the emitter converts them, so a program’s own method called take keeps its i64.

enumerate is not among them: the position it hands out is a usize, which is not a type a program meets (ADR-048 D1), and converting inside a tuple is a shape of its own.

4. What is left out on purpose

A stream consumed from both ends at once — a cursor with a next() and a next_back(), narrowed from both sides. A Seq has no next() at all; it is walked by for and the consumers, and a cursor is a new abstraction rather than a word on this one. Narrowing text or a list from both sides is index arithmetic on views and works today without allocating (ref t[i..<j] is &t[i..j] below); a parser that narrows a stream goes through a grammar. The owner decided to leave it out until a program needs it.

5. Found on the way, and not decided here

All answered since: the first bullet by ADR-213, the last by ADR-214, the rest by ADR-215.

6. What it costs

A kept range is two integers and a copy per walk; the copy is what the language below’s own Range does on every clone(), and the stepping inlines to the same loop. A range in a for is unchanged. The checker reads three more words on a type it already compared, and walks one more stack of frames on a read of a sequence name.

7. What is built

All of it, in 0.0.188: Shape in contracts::ty and its parsing, writing, fitting and passing through; the ends_by_length column; NK2703; the range’s type; the taking at every read, in loops and lambdas; owned_loops and count_args from the checker to the emitter; nikaia_std::range; the entries of D5; examples/trend/src/main.nika, a program that walks three pipelines from their back ends and a range twice; and crates/nikaia/tests/sequence_shapes.rs, each program compiled and run at both settings of user_parallelism.

This site is open source. Improve this page.