Skip to content

Give repetition, exchange and drains forms of their own - #44

Merged
zmaril merged 3 commits into
mainfrom
pr4-repeat-swap-drain
Aug 25, 2026
Merged

Give repetition, exchange and drains forms of their own#44
zmaril merged 3 commits into
mainfrom
pr4-repeat-swap-drain

Conversation

@zmaril

@zmaril zmaril commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Stacked on #43. Normalization only — no findings change in this PR on either
corpus. It is vocabulary, and two of the three laws are reported with the number
they actually moved, including one that is zero.

Repeat and Swap

Three of seven hand-written sort spellings were opaque at the top, because a
while was raw syntax and described no work at all: a library function that
loops that way yielded no behavior. Repeat holds the construct, and a loop
is the same construct with its condition written inside as a break — so
loop { if done { break } .. } reduces to while !done { .. }. A loop with a
second way out keeps its shape, because hoisting only the first test would claim
it runs longer than it does.

Swap is the operation every naive sort is built from. v.swap(i, j) and the
three statements through a temporary shared no subterm; the corpus writes the
second 131 times. With those, and a group of one step reducing to that step,
the two spellings of a bubble sort reduce to one form. That is not sort
detection and should not be mistaken for it — it means the shape space stopped
growing with the spelling.

The counted loop — correct, and it absorbs nothing

let mut i = 0; while i < n { .. ; i += 1 } is for i in 0..n. This law only
has to reach a walk over a span; the element-traversal law from #41 takes it the
rest of the way, so the while spelling lands on the identical form the for
spelling does, and a loop with a leading break reaches it through four laws.

Unlike every other law here it is not local to a node: the span's START is in
the binding before the loop, so the two have to be seen together.

Writing down where the step sits found a bug in the first version. Counting
up, the increment goes last and the walk is a..n. Counting down, the decrement
goes FIRST — that is what keeps the index in range and how the loop is written —
and the walk is the same span the other way about. The other two placements
visit different spans and are refused. rev now flips a walk's direction too,
which nothing produced before.

MEASURED, AND THE NUMBER IS ZERO. Across 727 CodeNet files it absorbs no
repetitions: 590 hold one with the law and 590 without. The reason is plain in
what those loops are — 434 are loop with the exit deeper in, and most of the
rest are binary searches and two-pointer walks whose index jumps rather than
steps. In 575 functions of production Rust only 15 repetitions survive and 13
are while let. Rust programmers write for when they mean a counted loop.

Kept because it is correct, costs nothing, and a while-spelled distinctness or
sortedness check is now recognized where it was not — both verified end to end.
Reviewers may reasonably disagree and I would not argue hard for it.

while let, and the drain — this is where the loops were

while let P = e { b } is loop { match e { P => b, _ => break } }, the
desugaring the language performs. Writing it out fixes something worse than a
missing law: the condition was normalized as an expression, so the name the
pattern binds came out a free variable — a hole matching anything, where the
body reads a binding.

as_drain then reads taking from a container until it is empty as walking it,
so while let Some(x) = queue.pop_front(), while let Some(x) = it.next() and
for x in queue reduce to one form.

Two refusals carry it, both measured first:

  • a body that puts something back is a worklist, not a drain, and its elements
    are not the ones the container started with — 1,353 against 583
  • a bare pop is not taken at all, because the name settles no order: it is the
    last element of a Vec and the greatest of a BinaryHeap. Of the 931
    files draining with pop, 434 also use a BinaryHeap. Calling a heap's
    drain a backward walk would report the opposite order, which is the one thing
    Direction exists to prevent.

Measured on the population it is for: across 900 files that drain with
pop_front, pop_back or next, repetitions fall from 1,291 to 1,131.
160
loops that were opaque are now traversals.

Review notes

  • 355 tests, clippy clean.
  • Clippy scoreboard unchanged at 26/201, 36/36 on-target — verified against the
    base commit, so no regression. It is also no gain, and that is expected:
    needless_range_loop and manual_while_let_some are style lints about loop
    spelling, not "you reimplemented a library function", which is the only thing
    LibraryBehaviorMatch can say. notes/todo.txt:1000 called this in advance.
  • Sort is still not detected. The two blockers are the fixpoint loop
    (bubble-with-flag repeats on a flag, not a counter) and the early-stopping
    loop (insertion's while j > 0 && v[j-1] > v[j]). Neither is a normalization
    gap; both are genuinely different shapes.

@zmaril
zmaril force-pushed the pr4-repeat-swap-drain branch from 9456835 to 483c62d Compare August 25, 2026 18:50
@zmaril
zmaril force-pushed the pr4-repeat-swap-drain branch from 483c62d to 1691381 Compare August 25, 2026 20:28
Base automatically changed from pr3-std-catalog-is-sorted to main August 25, 2026 21:16
zmaril and others added 3 commits August 25, 2026 23:16
Three of seven hand-written sort spellings were opaque at the top, because a
`while` was raw syntax and described no work at all: a library function that
loops that way yielded no behavior. `Repeat` holds the construct, and a `loop`
is the same construct with its condition written inside as a `break` — so
`loop { if done { break } .. }` reduces to `while !done { .. }` and the two
spellings stop sharing nothing. A loop with a second way out keeps its shape,
because hoisting only the first test would claim it runs longer than it does.

`Swap` is the operation every naive sort is built from. `v.swap(i, j)` and the
three statements through a temporary are the same exchange and shared no
subterm; the corpus writes the second 131 times. The law is narrow — what is
saved must be what the first assignment overwrites, what that is overwritten
with must be what the second overwrites, and the temporary must be spent — so a
shift through three positions and a temporary read afterwards both decline.

With those, and with a group of one step reducing to that step, the two
spellings of a bubble sort reduce to ONE form. That is not sort detection and
should not be mistaken for it: what it means is that the shape space stopped
growing with the spelling.

Nothing already found changed: 11 and 30 on CodeNet, still zero on production
Rust. The normalizer moved under both and the findings did not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ghYbUJFcVfkKYacbTVJsR
`let mut i = 0; while i < n { .. ; i += 1 }` is `for i in 0..n`, and the two
shared nothing. This law only has to reach a walk over a span — the element
traversal law already takes it from there — so the `while` spelling now lands on
the identical form the `for` spelling does, and a `loop` with a leading `break`
reaches it through four laws in a row.

Unlike every other law here it is not local to a node: the span's START is in
the binding before the loop, not in the loop, so the two have to be seen
together and that means matching on the sequence they are steps of.

Where the step sits is part of what the loop visits, and writing that down found
a bug in the first version. Counting up, the increment goes last and the walk is
`a..n`. Counting down, the decrement goes FIRST — that is what keeps the index
inside the sequence, and it is how the loop is actually written — and the walk
is the same span the other way about. The other two placements visit different
spans and are refused rather than quietly given their sibling's. `rev` on a
sequence now flips a walk's direction too, which nothing produced before, so a
descending counter and a reversed range agree.

The side conditions all earn their place, each with a test: a step inside a
branch may not happen, a stride is not a span, a counter read afterwards is a
value the rewrite would delete, and a body that can move the limit is not
walking a fixed one. That last one has no effects to consult, so it asks what it
can — a name the limit depends on may be read and not assigned to, called on, or
swapped through — with one exemption that nested loops need: working the limit
out again is reading it, not changing it.

MEASURED, AND THE NUMBER IS ZERO. Across 727 CodeNet files the law absorbs no
repetitions at all: 590 forms hold one with the law and 590 without. The reason
is plain in what those loops are — 434 are `loop` with the exit deeper in, and
most of the rest are binary searches and two-pointer walks whose index jumps
rather than steps. In 575 functions of production Rust only 15 repetitions
survive and 13 of those are `while let`. Rust programmers write `for` when they
mean a counted loop, and reach for `while` precisely when `for` will not do.

The law is kept because it is correct, costs nothing, and a `while`-spelled
distinctness or sortedness check is now recognized where it was not — both
verified end to end. But it is not the unlock, and `while let` is where the
loops actually are.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ghYbUJFcVfkKYacbTVJsR
`while let P = e { b }` is `loop { match e { P => b, _ => break } }` — not an
analogy, the desugaring the language performs — and writing it out that way
needs no new vocabulary. It also fixes something worse than a missing law: the
condition was normalized as an EXPRESSION, so the name the pattern binds came
out a free variable. A hole matching anything, where the body reads a binding.

On top of that, `as_drain`: taking from a container until it is empty reaches
every element exactly once, which is what walking it does. So
`while let Some(x) = queue.pop_front()` and `for x in queue` now reduce to one
form, and so does a hand-written `while let Some(x) = it.next()`.

Two refusals carry the law. A body that puts something back is not draining —
it is a worklist, and its elements are not the ones the container started with;
measured, that is the commoner shape by two to one, 1,353 against 583. And a
bare `pop` is not taken at all, because the name settles no order: it is the
last element of a `Vec` and the GREATEST of a `BinaryHeap`. That is not a corner
case here — of the 931 files draining with `pop`, 434 also use a `BinaryHeap` —
and calling a heap's drain a backward walk would report the opposite order,
which is the one thing `Direction` exists to prevent.

Measured on the population it is for: across 900 files that drain with
`pop_front`, `pop_back` or `next`, repetitions fall from 1,291 to 1,131. A
hundred and sixty loops that were opaque repetitions are now traversals, and
comparable to every `for` loop in the corpus. The counted-loop law absorbed
exactly none; this is where the loops were.

Findings unchanged either way: 11 and 30 on CodeNet, zero on production Rust.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ghYbUJFcVfkKYacbTVJsR
@zmaril
zmaril force-pushed the pr4-repeat-swap-drain branch from 1691381 to f863dd1 Compare August 25, 2026 21:16
@zmaril
zmaril merged commit 3fcdf75 into main Aug 25, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant