ADR-172: A for May Iterate Something Whose Step Pauses

Status: Accepted Date: September 20, 2026 Related: ADR-105 D1 (Seq[T], and the words after it that say what one step may do), ADR-025 D6 (throws on a step, the shape this copies), ADR-121 D4 (a worker’s reply as something that can be awaited), ADR-055 §5 (binding tokio is rejected), ADR-303 (the I/O layer this language already has), ADR-119 D2 (the no_std target a dependency would have to fit), Part III 17.1 (a stream larger than memory is a for over io::lines())

1. Context

Before this record, for line in io::lines() { … } lowered to this:

for line in io::lines().await {
    let line = line?;
    …
}

The call awaited, because io::lines is not sync. The step did not: Lines::next was an Iterator::next over a BufRead, which blocks the thread it is on for as long as the line takes. So a for over standard input held its thread for the whole loop, and at user_parallelism = yes that is a thread the pool could have had.

It is a question about what a for is in this language and not about how to build one. Rust has no stable Stream, so a step that pauses is while let Some(x) = s.next().await below, and deciding that a for may mean that is a decision about the construct.

The shape already existed. ADR-025 D6 put a word on a type that changes how the emitter writes a for’s step — throws, answering can it fail. A second word on the same type, answering does it pause, is one more of the same thing rather than a new idea. And a for that cannot pause makes user_parallelism = yes a promise the standard library itself breaks.

2. Decisions

D1 — A Seq says whether its step pauses, and a for over one gives its thread up.

The word is pauses, after the sequence and beside sync and throws:

signature = "() -> Seq[String] pauses throws"

A for over such a sequence is written as one that awaits each step. Nothing is refused and nothing is reported: a for over a stream is an ordinary program, and the whole of this is that it stops holding a thread it does not need.

Three states and not two, and that is the decision rather than a detail. The absence of sync was already readable as a step may pause, and taking it as the claim would have been cheaper by one word. It is wrong, and the two ways of being wrong are not symmetric:

So the claim is positive. sync is a promise (it does not pause), pauses is a warning (it does), and neither is nobody said — which is what a map’s step is, since it runs the lambda and what the lambda does is the caller’s. A ledger that writes both words has said two things about one step and is not read as either.

The verdict travels with the type, so naming the stream first is the same loop: let stream = io::lines() and for line in stream await exactly as the inline form does.

D2 — The awaiting form is written off the checker’s answer, never off the shape.

Which sequences pause is a claim in the ledger about a type, and the emitter has no types (ADR-288). So the checker records the loops whose step pauses by the byte the statement starts at and hands the set over — the same arrangement fallible_loops has, for the same reason and stated in the same place.

The two compose without either knowing about the other: a loop that both pauses and can fail awaits its step and takes the ? inside, which is one ledger word apart from a loop that does neither.

D3 — The trait, when there is one, is std’s own.

There is no trait yet, and that is the scope rather than a gap. Of Seq[T]’s six producers in std.contracts — io::lines, HashMap::keys, HashMap::values, String::chars, Vec::drain, HashMap::drain — five are sync and exactly one can pause. What the emitted while let wants is an inherent async fn next on one concrete type; a trait over many is owed the day there is a second pausing producer, and is one trait rather than a language feature.

And it is not a foreign one, which was asked and is answered here so that it is not asked again:

So the trait is ours when it arrives, which is the move Joined made in ADR-280. futures_core::Stream earns its place only at a foreign boundary, where a described crate hands us a stream and its trait is the lingua franca — never in the core.

D4 — io::lines reads a chunk at a time, and the line endings are found here.

The first producer, and the one D1 was asked for. A step of it is a read of standard input on an I/O worker, awaited, which is ADR-121 D4’s shape.

A chunk and not a line, and this is the difference between a change that pays for itself and one that does not: a hop to a worker and back costs a wake-up, and paying one per line would make io::lines slower than the blocking reader it replaces. So Lines asks for a buffer, hands out the lines in it, and asks again when it runs out — one hop per buffer.

One read per hop and not a loop to fill the buffer. Short is not the end of a stream, and a caller that asked for a chunk wants whatever is there now: a pipe that produces a line a second is a program that prints a line a second, and filling 8 KiB first would make it a program that prints nothing for two hours.

What is handed out is dropped from the buffer before the next chunk goes in, so what is held is the size of the unread remainder and not of everything the stream ever carried. Part III 17.1’s a stream larger than memory is a for over this stays true, which is the property this could most easily have lost.

BufRead::lines’ own rules are kept, because programs were written against them: the newline is not part of the line, a \r before it is not either, a blank line is a line, and a final line without a newline is still a line — but only once the stream is over, or a chunk boundary would split a line at whatever byte it landed on.

D5 — An eager walk of a pausing sequence pauses and can fail; a lazy one is refused.

Seq[T]’s six consumers split on what they hand back, and the split is the rule rather than a list of names:

error: `map` walks a sequence whose step pauses, and a `for` is the only walk
       of one this compiler can write yet (ADR-172 D5). Write the loop -
       `for line in io::lines() { … }` - and do inside it what this was going to
       do afterwards
  --> app.nika:4:5
   4 |     let ls = io::lines().map fn { a }
           ^

A refusal from the lowering and no number, which is ADR-171 §4’s rule: a number is a promise the rule stays, and this is this compiler cannot build that yet rather than the language forbids it. The day the lazy ones exist, the sentence goes and nothing in the catalogue has to be withdrawn.

The failing half is NK2701, the same code the for gets. ADR-025 D1 says a step that can fail fails the enclosing function and that nothing marks the call; a count is a written call, so the rule already covered it and only the compiler did not. The message says a step of what count walks rather than a turn of this loop, because that is what a reader is looking at.

And that half’s absence would be a wrong answer, not a missing feature. A Lines that were an Iterator over Result<String, …> would have io::lines().count() count the failures as lines under a ledger that says -> i64, and collect hand back a list of results typed as a list of strings — the bug class Part I 6.4 refuses by name.

One thing the emitter had to stop guessing. collect is written .collect::<Vec<_>>() below, because Rust’s needs to be told what to build and this emitter has no types. Lines::collect takes no type at all, so the turbofish is skipped exactly where the site pauses — an Iterator::collect never does, which makes that the difference rather than the method’s name.

3. Consequences

A for over standard input gives its thread up. At user_parallelism = yes that is a thread the pool gets back, and at no it is the same program it was.

What it costs: one word in the ledger’s type language, one set threaded from the checker to the emitter, and one buffer per stream. Lines stops being an Iterator, which nothing outside std could see — the ledger names the type and a program writes for.

What it does not change: every other loop. A container, a range, and a sequence that says nothing keep the shape they had, which is asserted rather than hoped for.

4. What this does not decide

This site is open source. Improve this page.