Give repetition, exchange and drains forms of their own - #44
Merged
Conversation
zmaril
force-pushed
the
pr4-repeat-swap-drain
branch
from
August 25, 2026 18:50
9456835 to
483c62d
Compare
zmaril
force-pushed
the
pr4-repeat-swap-drain
branch
from
August 25, 2026 20:28
483c62d to
1691381
Compare
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
force-pushed
the
pr4-repeat-swap-drain
branch
from
August 25, 2026 21:16
1691381 to
f863dd1
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
RepeatandSwapThree of seven hand-written sort spellings were opaque at the top, because a
whilewas raw syntax and described no work at all: a library function thatloops that way yielded no behavior.
Repeatholds the construct, and aloopis the same construct with its condition written inside as a
break— soloop { if done { break } .. }reduces towhile !done { .. }. A loop with asecond way out keeps its shape, because hoisting only the first test would claim
it runs longer than it does.
Swapis the operation every naive sort is built from.v.swap(i, j)and thethree 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 }isfor i in 0..n. This law onlyhas to reach a walk over a span; the element-traversal law from #41 takes it the
rest of the way, so the
whilespelling lands on the identical form theforspelling does, and a
loopwith a leadingbreakreaches 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 decrementgoes 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.
revnow 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
loopwith the exit deeper in, and most of therest 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 writeforwhen they mean a counted loop.Kept because it is correct, costs nothing, and a
while-spelled distinctness orsortedness 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 werewhile let P = e { b }isloop { match e { P => b, _ => break } }, thedesugaring 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_drainthen 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()andfor x in queuereduce to one form.Two refusals carry it, both measured first:
are not the ones the container started with — 1,353 against 583
popis not taken at all, because the name settles no order: it is thelast element of a
Vecand the greatest of aBinaryHeap. Of the 931files draining with
pop, 434 also use aBinaryHeap. Calling a heap'sdrain a backward walk would report the opposite order, which is the one thing
Directionexists to prevent.Measured on the population it is for: across 900 files that drain with
pop_front,pop_backornext, repetitions fall from 1,291 to 1,131. 160loops that were opaque are now traversals.
Review notes
base commit, so no regression. It is also no gain, and that is expected:
needless_range_loopandmanual_while_let_someare style lints about loopspelling, not "you reimplemented a library function", which is the only thing
LibraryBehaviorMatchcan say.notes/todo.txt:1000called this in advance.(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 normalizationgap; both are genuinely different shapes.