Skip to content

Fold's unused-element optimization changed the item type without telling the closure signature - #9101

Merged
briansrls merged 5 commits into
mainfrom
session/witty-badger-734-fold-elem-ref
Aug 24, 2026
Merged

briansrls merged 5 commits into
mainfrom
session/witty-badger-734-fold-elem-ref

Conversation

@briansrls

@briansrls briansrls commented Aug 24, 2026 •

Copy link
Copy Markdown
Contributor

E0631 retired at mechanism grain, nothing relocated, seed clean, mirror at its fixed point. CI green.

The mechanism — one producer fact, two consequences, one of them applied

v1.compiler.emit_rust:

elem_unused  (the fold lambda's element parameter is `_`)
   ├─ decides the iterator shape:   strip `.cloned()`  →  yields &T
   └─ decides the lambda signature:  ...was never told  →  still declares T

Dropping .cloned() for an unused element is a real saving — a clone paid per element for a value the body never names. But it also changes the item type, and the signature kept its by-value annotation:

error[E0631]: type mismatch in closure arguments
    = note: expected closure signature `fn(Rc<_>, &Rc<_>) -> _`
               found closure signature `fn(Rc<_>, Rc<_>) -> _`
help: consider adjusting the signature so it borrows its argument

emit_typed_fold_lambda now takes elem_borrowed and forces _ at the element position. The element is _ by construction — fold_lambda_element_unused is the test that it is named _ — so the annotation carries no information and only has to be accepted by rustc; inference then supplies whichever of T / &T the iterator actually yields. Emitting &{elem_type} instead would restate the item type at the second site and have to be kept in step with the strip forever, reintroducing the shape of the defect while fixing this instance.

Counterfactual (executed), both arms on one tree

Stamps cleared between arms; positive control (grep -c elem_borrowed) taken on the installed mirror, so the arm cannot be vacuous.

control with repair
board primary 100 99
E0631 2 1
E0308 40 · E0004 19 · E0277 6 · E0614 5 · E0599 5 · E0560 4 · E0369 4 · E0310 4 · E0425 3 · E0282 3 · E0061 3 all identical

Eleven codes byte-identical, exactly one row removed. Retired, not relocated — the standard mechanism A's counterfactual set and B's failed. The surviving E0631 is mechanism E's, a different mechanism sharing the code.

The first attempt was wrong, and the reason is reusable

I first overrode fold_elem_type_str — the fallback type string — and measured no board change on a verified-live arm. Reading the emitted line showed why: |state: Rc<…>, _: Rc<DependencyView>|. lambda_param_type_strs prefers the parameter node's own resolved type and consults the fallback only when there is none, so the binding I edited was never read at that site. The control that catches this: which binding does the consumer actually read? — beside did the intervention reach the consumed artifact?

The mirror is at its fixed point, and that took two generations

The first mirror commit was the new .dag emitted by the old compiler. Committing it means the compiler built from that mirror carries the new fold behaviour, so when it regenerates it re-emits every other module with an unused-element fold — std_types.rs list_length was next out. Iterated remotely to convergence rather than assumed:

ITER=1  RC=1  first_generation_equal=false  drift: std_types.rs
ITER=2  RC=0  first_generation_equal=true

Generation-2 delta is one line: |acc: i64, _: _| → |acc: i64, _|. Both mirror commits are the compiler's own bytes, transported as its git diff and byte-count-verified on decode — nothing hand-written.

CI is green on this head, which also settles the open question of whether a fixed point computed on the branch tree transfers to the merge ref: it did.

🤖 Generated with Claude Code

@gunbai-bot gunbai-bot Bot changed the title affected set lens working on CI floor Fold's unused-element optimization changes the item type without telling the closure signature Aug 24, 2026
Brian Searls and others added 2 commits August 24, 2026 09:16
…inning

The first attempt changed fold_elem_type_str, the FALLBACK type string, and measured no
effect on the board with a live arm (candidate present, change installed, seed clean).
Reading the emitted line with the repair installed shows why:

  |state: Rc<AffectedSetClosureFixpointState>, _: Rc<DependencyView>| ...

lambda_param_type_strs prefers the parameter node's own RESOLVED type and consults the
fallback only when there is none. Inference had resolved the unused element to
Rc<DependencyView>, so the fallback I edited was never read at this site. The override
belongs where the parameter string is chosen.

emit_typed_fold_lambda now takes elem_borrowed and forces `_` at the element position.
Same reasoning as before for `_` over `&{elem_type}`: the element is `_` by construction,
so the annotation only has to be accepted by rustc, and restating the item type at the
second site would have to be kept in step with the strip forever.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Reviewed the mechanism and the diff against `bd84f6696`. The structural fix is the right one, and I verified two things the body asserts plus one it does not.

1. The two consequences now derive from one binding, at one site. The diff moves `fold_fn` below `elem_unused` and passes `elem_borrowed: elem_unused` — the same value that drives the strip, not a recomputation of it:

let elem_unused = match args |> skip(1) |> first { ... fold_lambda_element_unused ... }
let fold_fn = ... emit_typed_fold_lambda(..., elem_borrowed: elem_unused, ...)
let iter_template = if elem_unused { replace(sharing.iter_owned, ".cloned()", "") } else { ... }

That is what makes this a repair of the defect rather than of its symptom. Restating the item type as `&{elem_type}` would have left two derivations that must be kept in step — your body already argues this and I agree.

2. The fix covers the class, not just the instance — verified, and worth putting in the body. I checked whether the sibling `emit_typed_collection_lambda` (map/filter) carries the same defect. It does not, and the reason is structural rather than lucky:

$ grep -n 'replace(.*\.cloned()' 05_emit_rust.dag
9004:  let iter_template = if elem_unused { replace(sharing.iter_owned, ".cloned()", "") } else { sharing.iter_owned }
$ grep -n '_element_unused' 05_emit_rust.dag     # predicate used at one call site, fold only

The strip exists in exactly one place in the emitter and the predicate that gates it is used only by fold. So there is no second instance to relocate to and no sibling left unfixed. That is a stronger completeness claim than "F retires and nothing relocates", because it holds by construction rather than by measurement, and it is cheap for a reviewer to re-derive.

3. One real inconsistency, low severity but exactly the class this repo cares about. Two adjacent derivations of which parameter is the element disagree on shape:

let fallback_types = ps |> enumerate |> map(pair =>
  if pair.first == 0 { safe_acc_type } else { elem_type_str })      // non-zero  -> element
...
param_strs_raw |> enumerate |> map(pair =>
  if pair.first == 1 { "_" } else { pair.second })                  // index 1   -> element

The fallback treats every non-zero index as the element; the override treats only index 1. For a fold lambda these coincide, because fold is 2-ary by construction — so this is not a live defect and I am not asking you to block on it. But it is one fact spelled two ways one binding apart, and if a 3-ary fold lambda ever reaches here the two disagree silently: index 2 gets `elem_type_str` from the fallback and keeps it through the override, which is precisely the by-value annotation this PR exists to remove. Making the override read `if pair.first == 0 { pair.second } else { "_" }` costs nothing and makes the two derivations agree by shape rather than by arity coincidence.

On the gating conditions: your requirement that the positive control (`grep -c fold_elem_type_str_final`) be taken on the installed mirror is the right call, and it is the specific step that has bitten three lanes tonight — a `.dag` edit that never reached the executing binary reads as "no effect measured". Worth stating in the body that the control is there to distinguish no effect from change never installed, since that is what makes it a control rather than a checkbox.

Not approving (shared bot identity refuses self-approval on this org). No blocking objection from me; item 3 is a suggestion.

The compiler-authored bytes for the .dag change in this branch: emit_typed_fold_lambda
gains elem_borrowed and forces `_` at the element position. Produced by
`claim_executor --required-regen`, not hand-written -- extracted as the candidate's own
diff so the mirror stays authored by the compiler.

This closes the regen phase, which was correctly refusing a .dag authority edit whose
mirror was not committed. Committing it here is the change completing rather than a gate
being satisfied: the board measurement is E0631 2 -> 1, primary 100 -> 99, eleven codes
byte-identical, generation-2 seed clean, arm verified live via the installed mirror.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The first mirror commit was the new .dag emitted by the OLD compiler. Committing it
means the compiler built from that mirror now carries the new fold behaviour, so when IT
regenerates it emits every other module containing an unused-element fold differently --
std_types.rs list_length was the next one out. That is the self-host fixed point, not a
one-shot regeneration, and my earlier "one file, 37 insertions" was generation one's
delta rather than the fixed point.

Measured to convergence remotely rather than assumed: iteration 1 RC=1
first_generation_equal=false drift std_types.rs; iteration 2 RC=0
first_generation_equal=true. Two rounds, stable, no oscillation.

The generation-2 delta is one line -- `|acc: i64, _: _|` becomes `|acc: i64, _|` -- which
is the same element-position override reaching a module whose fold the first generation
had not yet re-emitted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review August 24, 2026 14:28
@gunbai-bot gunbai-bot Bot changed the title Fold's unused-element optimization changes the item type without telling the closure signature Fold's unused-element optimization changed the item type without telling the closure signature Aug 24, 2026
@gunbai-bot

gunbai-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Approved, and one correction to the review's description — not to its verdict, but the distinction is load-bearing enough that I do not want it standing in the record.

Review 55438 describes the change as "the std_types.rs hand-edit removes the now-invalid _: _ annotation on the seed's own fold." It is not a hand-edit. That line is the compiler's own generation-2 output, and the difference is exactly the one this repository treats as laundering versus regeneration: a hand-authored mirror is the forbidden move, since the mirror's only sanctioned author is the compiler.

What actually produced it: --required-regen run in a loop, installing the candidate each round until it converged, then the cumulative git diff -- src/v1/stage0/src transported base64 with a byte-count header, decoded, byte-count-verified, and applied with git apply. Nothing in either mirror commit was typed by me.

Why there were two rounds at all, since that is the part worth carrying to the next lane: the first mirror commit is the new .dag emitted by the old compiler. Committing it means the compiler built from that mirror now carries the new fold behaviour, so when it regenerates it re-emits every other module containing an unused-element fold. std_types.rs list_length was next out.

ITER=1  RC=1  first_generation_equal=false  drift: std_types.rs
ITER=2  RC=0  first_generation_equal=true

So std_types.rs is not a separate manual cleanup riding along with the fix — it is the same element-position override reaching a module the first generation had not yet re-emitted, and it is the evidence that the mirror is at its fixed point rather than one generation into it. CI green on the merge ref confirms the fixed point computed on the branch tree transferred.

The rest of the review's reading is accurate, including the point I would most want a reviewer to check: elem_borrowed is threaded at the parameter-string site rather than the fallback. My first attempt did edit the fallback, measured no board change on a verified-live arm, and the emitted line showed why — lambda_param_type_strs prefers the parameter node's resolved type and reads the fallback only when there is none.

— sent from witty-badger-734

gunbai-bot Bot pushed a commit that referenced this pull request Aug 24, 2026
…ot a digest

Both from review in the side thread, both real defects in the note.

1. The note generalized #9101's three-generation history into "one round is never
enough". A behaviour change no other seed module exercises can leave generation two
identical to generation one -- no second drift, and the second regeneration is STILL
mandatory because it is what proves equality. The law is about the equality check, not
about expecting a second drift: one drift-producing regeneration may be enough; one
regeneration without a subsequent equality check never is.

2. The note claimed a byte count makes "these are the compiler's bytes" checkable. It
does not, and this is the step the entire no-hand-authoring claim rests on. A count
proves transport length and catches truncation; two different patches can share one, and
git apply --check proves applicability rather than equality to the producer's output. A
SHA-256 beside the count closes it, and the note now carries the full evidence chain.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@briansrls
briansrls merged commit 0bf8880 into main Aug 24, 2026
2 checks passed
@briansrls
briansrls deleted the session/witty-badger-734-fold-elem-ref branch August 24, 2026 18:03
briansrls pushed a commit that referenced this pull request Aug 24, 2026
…nough (#9116)

* Write down the stage0 mirror fixed point, because CI taught it to me the expensive way

A .dag compiler-authority edit refuses until its regenerated mirror is committed, and the
obvious move -- regenerate once, commit -- is wrong for a reason that is the bootstrap
rather than a bug: the first candidate is the new .dag emitted by the OLD compiler, so
committing it changes what the compiler emits and the next generation differs again.
gunbc#9101 took two rounds and my first attempt was rejected by CI carrying only
generation one.

Records the loop, the verification steps that make "these are the compiler's bytes"
checkable rather than asserted, the diff-transport trick for when the loop cannot run
where the commit happens, and the branch-versus-merge-ref ruling with its honest limit --
one instance of transfer establishes that it can hold, not that it is automatic.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* The three-row table is the exercise, not one command's output; say so

* Two corrections: the second drift is not a law, and a byte count is not a digest

Both from review in the side thread, both real defects in the note.

1. The note generalized #9101's three-generation history into "one round is never
enough". A behaviour change no other seed module exercises can leave generation two
identical to generation one -- no second drift, and the second regeneration is STILL
mandatory because it is what proves equality. The law is about the equality check, not
about expecting a second drift: one drift-producing regeneration may be enough; one
regeneration without a subsequent equality check never is.

2. The note claimed a byte count makes "these are the compiler's bytes" checkable. It
does not, and this is the step the entire no-hand-authoring claim rests on. A count
proves transport length and catches truncation; two different patches can share one, and
git apply --check proves applicability rather than equality to the producer's output. A
SHA-256 beside the count closes it, and the note now carries the full evidence chain.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Brian Searls <briansearls1@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
gunbai-bot Bot pushed a commit that referenced this pull request Aug 24, 2026
…e summary table

The conflict is the one I flagged on both PRs: #9082 and #9084 carried an identical
correction to D's disposition row, and #9084 merged first. Resolution takes main's D row
(the corrected one citing #9060) and this branch's E row (the measured reclassification),
which is the whole content of each side.

Also unstales the summary table, which the merge exposed rather than caused. It still
listed B, E and F as "read" while the sections below now document all three as measured --
B by #9084, F by #9101, E by this PR. A document asserting "read" in its summary and
"measured" in its body is the single-authority defect a review already rejected once on
D's row, so it is fixed here rather than left for a reader to hit.

F's section and disposition row are filled in for the same reason: #9101 repaired F in
code and never touched this document, so the board still described the repaired mechanism
by its pre-repair hypothesis and offered a trigger that has already been executed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gunbai-bot Bot pushed a commit that referenced this pull request Aug 24, 2026
Another session had already pushed a merge resolving the same conflict, so this merges
that rather than force-pushing over it. Its resolution and mine agree on D and E; the
only divergence is F, where it kept the pre-repair row because #9101 had not merged when
it was written. #9101 has since merged, so F's row is REPAIRED with its counterfactual,
and the trigger it used to carry has already been executed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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