Skip to content

Propagate the PartialEq bound one hop up the call graph: the caller that forwards its own generic param into a comparing callee emitted a program that does not typecheck - #8802

Merged
briansrls merged 1 commit into
mainfrom
session/zesty-bear-761
Aug 21, 2026

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Unblocks #8605's regen. Orphaned lane (swift-moth-294 no longer exists), picked up here.

The gap

#8699 landed the seed's equality bound correctly, but only for the function whose own body compares. That is not the 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 a program that does not typecheck, and the E0277 lands on a function whose body compares nothing.

Measured on the real specimen, #8605's std.trait_derive_shape:

symbol before
repr_grounding_derive_shape_has_trait<K> (compares k == capability_key) <K: Clone + PartialEq> ✅
repr_grounding_derive_completeness_predicate<K> (forwards its own K into that parameter) <K: Clone> ❌ E0277

What changed

  • The per-(callee param, call argument) join the Clone forwarding already performed is factored out as v1_call_forwarding_forwarded_param_names and is trait-agnostic: the only thing separating 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 it per trait would be a second representation of one fact about a call site (§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 (§2), then forwards it.
  • Its walk is recursive where the Clone half's is direct, and that is a difference in the specimens, not in taste: 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 this 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 (§6): a fn with no type params of its own 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

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

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 the first green surfaced

v1_union_bound_param_names filtered extra against base only, so a name repeated within extra survived twice. Latent while the sole 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 — one call site forwarding 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, so no consumer has to know whether the result can repeat.

Test plan — green by execution, with a discriminating red

🤖 Generated with Claude Code

…warding its own generic param into a comparing callee emitted a program that does not typecheck

#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: Claude Opus 5 (1M context) <noreply@anthropic.com>
@briansrls
briansrls merged commit 08be45b into main Aug 21, 2026
1 check passed
@briansrls
briansrls deleted the session/zesty-bear-761 branch August 21, 2026 20:57
gunbai-bot Bot pushed a commit that referenced this pull request Aug 21, 2026
… a STEP

33 -> 10 -> 0 hard diagnostics. The recurring cost of every integration that
touches src/v1/*.dag is that main's hunks arrive with bare names (main still
has imports) and the cut leaves them unqualified. This has now happened four
times and I have hand-fixed it four times, so it is a step rather than a
memory:

  scratchpad/qualify-from-diagnostics.py

It is driven by the COMPILER'S OWN DIAGNOSTICS, never by a corpus sweep: it
reads "unresolved type 'X' (file:...)" out of a regen log, so it only touches
names the compiler reported, at files the compiler named. Three properties
that matter, each one a defect I hit today:

  * its declarer index reads multi-line coproducts and payload-carrying
    variants, the blind spot that manufactured a 4067-row false finding and a
    189-row false audit
  * it keeps a declaration head and a variant name bare and qualifies only
    the rest of the line -- the guard that previously skipped whole variant
    lines and left their FIELD TYPES unqualified
  * a name with two or more declarers is REFUSED and reported, never guessed.
    No best-effort arm; ambiguity is a hand decision

It closed 8 of this integration's 33. The remaining class it CANNOT see is
the v1/v2 Node collision, which arrives as "no field 'name' on type 'Node'"
rather than as an unresolved name -- bare Node binds to v2.std.node.Node,
which has no such field. Ten sites, qualified to v1.std.core.Node by hand;
teaching the script that class needs the declarer to be chosen by the FILE'S
HOME rather than by uniqueness, which is a different rule and is not folded
in on a guess.

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