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)
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.
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.
Seq[T] say what the sequence is as a whole.Beside the step’s sync, pauses and throws:
ends — it can be walked from the back. A range, a list’s iter(),
drain(), chars(), windows, chunks.sized — its length is known before it is walked. The same, and a map’s
keys(), values() and drain(); not chars(), and not a filter.replays — walking it does not use it up. Only a range (D3).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).
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.
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.
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.
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.
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.
All answered since: the first bullet by ADR-213, the last by ADR-214, the rest by ADR-215.
String reaches rustc:
m["a"] = 1 and m[key] = 1 on a HashMap[String, _] (index::at has no
arm for a String, and a literal is not converted there). Every example keys
by ref String, which works. Answered by ADR-213, 0.0.189.HashMap::insert has no entry, so m.insert(k, v) is not checked; the
language’s form is m[k] = v.str::chars says -> ?, where String::chars says Seq[char] sync ends.chunks and windows hand out
ref Vec[$T], whose len and iter agree with the slice below.rustc an integer type to guess; it did before this record.s.count() + s.count())
are not told apart, because NK2702 orders reads by statement, as NK2101
does.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.
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.