Skip to content

Row 1b: derive Clone bound for call-forwarding fn/impl signatures - #8539

Merged
briansrls merged 2 commits into
mainfrom
session/clever-seal-633
Aug 19, 2026
Merged

briansrls merged 2 commits into
mainfrom
session/clever-seal-633

Conversation

@gunbai-bot

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

Copy link
Copy Markdown
Contributor

Summary

Extends the Clone-bound derivation mechanism landed in #8466 (TraitBoundWitnessScope::BoundedToDirectTopLevelMatchBody, the RC-match-scrutinee case) with a second, structurally distinct case: BoundedToDirectSingleCallLambdaBody. A wrapper function whose body is directly — or via a single-call lambda — a call into an already Clone-bounded callee now derives its own Clone bound from the callee's real requirement, rather than the emitter reasoning about it ad hoc per call site.

This targets Row 1b of the census in docs/probes/materialization_carriers_rebaseline_2026-08-19.md: 14 of the 51 baseline compile errors traced to generic fn/inherent-impl signatures missing a Clone bound their forwarding calls into other generic functions actually require.

What changed

  • src/v1/trait_bound_witness.dag — new pure decision core, v1_call_forwarding_clone_bound_wrapper_param_names. Scoped and documented as distinct from the RC-match mechanism; the next-rung trigger (a recursive body walker mirroring emit_rust_expr_if/emit_rust_expr_let's accessor shapes) is named as explicit future work, not built here (DESIGN.md §6 — no hypothetical generality).
  • src/v1/04_emit_info.dag, 04_infer.dag — add fn_decl_items: Map<String, Node> to EmitGraphInfo/EmitInfoBuildState, mirroring the pre-existing type_decl_items. This lets the new mechanism resolve a callee by name and re-run emit_fn_def's own derivation on the callee's real declaration Node, instead of a second parallel re-derivation (DESIGN.md §2/§5 — single authority, construction over validation).
  • src/v1/05_emit_rust.dag — minimal wiring at the emit_fn_def call site only: the existing self-derivation logic is extracted verbatim into v1_fn_body_derived_clone_param_names, and the new v1_call_forwarding_clone_bound_param_names is unioned with it. No new decision logic added to the hot emitter itself — all reasoning lives in the pure trait_bound_witness.dag module.
  • dag/test/claim/trait_bound_witness_test.dag — two new witnesses, both driving the real emit pipeline end-to-end via compile_dag_rust_emit_check (a real executing consumer, not an inert unit test — DESIGN.md §5):
    • call_forwarding_derives_clone_bound_from_callee: reproduces the real src/v2/std/staging.dag cached_stage<A, B> shape; asserts the fixed emitted signature fn cached_stage<A: Clone, B: Clone>(.
    • call_forwarding_scope_excludes_match_wrapped_body: a declared-boundary negative control (DESIGN.md §4b rung honesty). Reproduces src/v2/compiler/materialization_carriers.dag's cached_stage_governed<A, B> shape — a lambda body that wraps the call in a match rather than being directly the call — and confirms it stays bare (fn cached_stage_governed<A, B>(), outside this mechanism's declared scope.

Verification

Ran the census-doc's own repro instrument (docs/probes/materialization_carriers_rebaseline_2026-08-19.md §1): real gunbc compile --target rust → cssl_assemble → cargo build --release --lib --message-format=json against the assembled crate.

  • Baseline (main@72676cf0be): 51 errors — E0277 16 · E0308 15 · E0599 9 · E0425 3 · E0422 2 · E0369 2 · unreachable_patterns 2 · E0560 1 · E0282 1.
  • After this change: 0 E0277/E0308/E0599/E0369/unreachable_patterns/E0560/E0282 errors. 5 errors remain (E0422 x2, E0425 x3), both pre-existing and unrelated to Clone bounds (ProviderRetention/NonEmptyStr not found) — out of this PR's scope.
  • Grepped the assembled crate directly: v2_std_staging.rs now emits fn cached_stage<A: Clone, B: Clone>(...), confirmed against the real src/v2/std/staging.dag module.
  • v2_compiler_materialization_carriers.rs's cached_stage_governed<A, B> remains bare — the declared scope boundary above. Confirmed it has zero callers in this crate's dependency closure, which is why the residual gap does not surface as a live rustc error in the verified entrypoint.
  • All 3 witnesses in dag/test/claim/trait_bound_witness_test.dag (the pre-existing RC-match witness plus the two new ones) PASS via claim_batch --source-root dag --source-root src/v2 --entry dag/test/claim/trait_bound_witness_test.dag --functions ....

Test plan

  • claim_batch run over trait_bound_witness_test.dag: all 3 witnesses PASS
  • Full repro instrument from docs/probes/materialization_carriers_rebaseline_2026-08-19.md: 46 Clone-bound-related errors resolved, 5 pre-existing unrelated errors remain, byte-verified against real emitted Rust
  • Confirmed no conflict markers / clean merge after integrating origin/main

dashboard node adhoc-574e999b-39c

gunbc-ci-auto-heal added 2 commits August 19, 2026 08:33
Extends TraitBoundWitnessScope with BoundedToDirectSingleCallLambdaBody:
a wrapper function whose body is directly (or via a single-call lambda)
a call into an already Clone-bounded callee now derives its own Clone
bound from the callee's real requirement, instead of the emitter
reasoning about it ad hoc per call site.

- src/v1/trait_bound_witness.dag: new pure decision core
  (v1_call_forwarding_clone_bound_wrapper_param_names), scoped and
  documented as distinct from the RC-match-scrutinee mechanism (#8466);
  next-rung trigger (a recursive body walker) named as explicit future
  work, not built here.
- src/v1/04_emit_info.dag, 04_infer.dag: add fn_decl_items: Map<String,
  Node> to EmitGraphInfo/EmitInfoBuildState so the new mechanism can
  resolve a callee by name and re-run emit_fn_def's own derivation on
  the callee's real declaration Node, rather than a parallel
  re-derivation (DESIGN.md S2/S5).
- src/v1/05_emit_rust.dag: minimal wiring at the emit_fn_def call site
  only — extracts the existing self-derivation into
  v1_fn_body_derived_clone_param_names and unions it with the new
  v1_call_forwarding_clone_bound_param_names; no new decision logic in
  the hot emitter itself.
- dag/test/claim/trait_bound_witness_test.dag: two new witnesses
  driving the real emit pipeline end to end (compile_dag_rust_emit_check),
  reproducing the real src/v2/std/staging.dag cached_stage<A, B> shape
  (now emits `fn cached_stage<A: Clone, B: Clone>(`) and a declared
  negative control for the match-wrapped-body boundary this mechanism
  does not cover (src/v2/compiler/materialization_carriers.dag's
  cached_stage_governed<A, B>, confirmed still bare — a named, non-silent
  scope boundary, not a live compile error in the census entrypoint).

Verified against docs/probes/materialization_carriers_rebaseline_2026-08-19.md's
repro instrument: real gunbc compile -> cssl_assemble -> cargo build
--release --lib --message-format=json against the assembled crate now
reports zero E0277/E0308/E0599/E0369/unreachable_patterns/E0560/E0282
errors (down from the 46 Clone-bound-related baseline errors); the 5
remaining errors (E0422 x2, E0425 x3) are pre-existing and unrelated
(ProviderRetention/NonEmptyStr not found).

dashboard node adhoc-574e999b-39c
@briansrls
briansrls merged commit bf45578 into main Aug 19, 2026
1 check passed
@briansrls
briansrls deleted the session/clever-seal-633 branch August 19, 2026 16:22
gunbai-bot Bot pushed a commit that referenced this pull request Aug 20, 2026
Breaks the --required-regen population deadlock for the new module
dag/extdeps/languages/rust/derive_contracts.dag: its first stage0 mirror
cannot be produced by regen itself, because validate_compared_populations
refuses on emitted_not_committed BEFORE any candidate is written.

Bytes are emitter output (gunbc compile --target rust --entry ...), never
hand-authored, following the route receipted by #8539. Registration is the
file plus its lib.rs line only -- neither stage0_emit_plan_generated.dag nor
stage0_crate_layout_generated.dag enumerates ordinary emitter-produced
modules (verified, not assumed).

Generation-1 mirrors installed: emit_rust, trait_derive_emit, derive_contracts.
Generation-2 is NOT installed and this tree is not the regen fixed point --
installing it yields 161 rustc errors, tracked separately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
briansrls added a commit that referenced this pull request Aug 21, 2026
…8574)

* Row 1a: supplemental generic Clone bounds for derives over FreeMonoid fields

Restores the row-1a mechanism from quarry after the §3 question that caused
its drop was settled by execution: #8539 landed and left these sites standing,
so its Clone-bound synthesis demonstrably does not generalise to this case.

The live instance is FreeMonoidUniqueState<T> in src/v2/std/algebra.dag, whose
derive over `seen: Rc<Vec<T>>` requires a supplemental T: Clone the emitter did
not synthesize (10 E0277 + 2 E0369 on the materialization_carriers board).

The witness oracle is silent-raven-853's, resurrected from their three commits
(c76b769 introducing it, a52f0e1 and 776a6d5 correcting it) rather
than rewritten. Author-of-oracle and author-of-implementation stay separate.

Re-attributes two notes that labelled a coordinating session's ruling as an
"operator verdict" while naming a session id. The substance is unchanged; the
label claimed an authority external to the diff that a session cannot hold.

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

* E0310: synthesize 'static on emitted impl Fn parameters

An arrow type realizes in Rust as Rc<dyn Fn(..) -> ..>, and a dyn trait object
defaults to the 'static lifetime, so a function that closes over its arrow-typed
parameters and returns one cannot compile unless those parameters are 'static.
rustc reports E0310 at the Rc::new(move |..| ..) site.

The obligation is created by the realization choice, not by the authored source:
v2/std/staging.dag declares plain arrows and carries no Rust concept at all. It
is fixed at the emitter for that reason. Declaring it at the source would mean
inventing Rust lifetime syntax in a medium-agnostic language ('static appears in
no .dag source module) and would leave the next arrow-into-Rc site to be
rediscovered the same way.

Unconditional rather than gated on reaching a stored position: the substrate has
no reference or lifetime concept, so every closure it can construct is already
'static and the bound rejects no expressible program. Same shape as the + Clone
beside it.

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

* Row 1a: drop two in-body source annotations the grain rule refuses

The restored mechanism carried five // lines inside declaration bodies.
DESIGN §4c admits only standalone leading blocks attached to module-scope
declarations, so v2 self-compile refused with two hard diagnostics and the
regen never produced a seed:

  source annotation sits inside a declaration body. Only module-item grain
  is modeled; move it above the declaration it describes.

Restores the quarry's own fix for this (8edf566), which the enumerated
commit list used to restore the mechanism had omitted.

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

* Row 1a witness: assert struct impl BODIES, not only their headers

The struct and enum witnesses were asymmetric. The enum asserted both
impl headers and impl bodies; the struct asserted headers only, so an
emitter producing correct headers over an empty or wrong body satisfied
every assertion in it -- a PartialEq whose body is literally `true`
passed. The enum's body assertions would have caught that only by being
the more thorough of the pair, which is luck rather than coverage
wherever the struct and enum emission paths can diverge.

The body spellings are grounded in a mechanism this change did not
author: the runtime prelude's pre-existing NonEmptyVec hand-written
impls establish the `&self.<field>` access form and the
`fn eq(&self, other: &Self) -> bool` signature, and
`debug_struct(..).field(..).finish()` is the std Formatter builder
documented for named-field structs. Reading the spellings off this
emitter instead would have re-run the same collision one level down.

That precedent also independently corroborates the two impl headers
already asserted here, which was not why it was consulted: it emits
`impl<T: Clone + std::fmt::Debug> std::fmt::Debug for NonEmptyVec<T>`,
character-for-character the shape of the third include, predating this
change. Those two assertions are therefore precedent-backed rather than
author-chosen -- load-bearing, because the rest of this witness was
authored by the session that authored the emitter under test.

Two limits recorded rather than left implicit: the include assertions
are spelling-sensitive, so a semantically identical where-clause form
fails include 3 and a red here is not by itself a semantic defect; and
the `#[derive(Debug` exclude matches file-wide, safe only while this
fixture declares a single type.

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

* Regen v1 emitter mirrors and seed the derive_contracts first mirror

Breaks the --required-regen population deadlock for the new module
dag/extdeps/languages/rust/derive_contracts.dag: its first stage0 mirror
cannot be produced by regen itself, because validate_compared_populations
refuses on emitted_not_committed BEFORE any candidate is written.

Bytes are emitter output (gunbc compile --target rust --entry ...), never
hand-authored, following the route receipted by #8539. Registration is the
file plus its lib.rs line only -- neither stage0_emit_plan_generated.dag nor
stage0_crate_layout_generated.dag enumerates ordinary emitter-produced
modules (verified, not assumed).

Generation-1 mirrors installed: emit_rust, trait_derive_emit, derive_contracts.
Generation-2 is NOT installed and this tree is not the regen fixed point --
installing it yields 161 rustc errors, tracked separately.

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

* Gate 'static synthesis on the enclosing fn returning an arrow

The unconditional form landed in 5d9fc3e on the premise that the bound
'cannot reject any expressible program'. That premise was FALSIFIED BY
EXECUTION: installing the generation-2 regen candidate produced 161 rustc
errors (E0373 / E0597) in v1_compiler_emit.rs and v1_compiler_emit_rust.rs.

The premise conflated WHAT is captured with HOW. The captured values are
owned, but the emitter emits the closure WITHOUT move, so the capture is a
borrow, and a non-move closure borrowing an owned local is not 'static
however owned that local is.

The gate is precise, not a proxy: the Rc::new(move |..| ..) site that creates
the obligation exists exactly when the function returns an arrow, and the
lowering already emits those closures with move. So no call site changes and
no additional clone is emitted. The rejected alternative -- emit move at every
closure site to satisfy a bound synthesized at every parameter -- is two
unconditional widenings composing, and a move closure must clone anything
still live afterward, raising clone count on the axis this lane is reducing.

Measured: the seed closure has 99 impl Fn parameters and ZERO functions
returning Rc<dyn Fn>, so the unconditional form bought nothing there. After
the gate, emitted '+ 'static' bounds in the regenerated seed closure: ZERO
(the two remaining matches are a hand-maintained file's own bound and this
emitter's own string literal). Regen drift falls from 8 files to 2.

Not yet at the fixed point: std_occurrence_binding.rs carries a SEPARATE
regression -- AmbiguousBindingCandidates<N> loses a required N: Clone through
an Rc-wrapped field -- which is Row 1a header-bareness, a different class,
tracked separately and not repaired here.

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

* Row 1a: split the fused impl_bodies flag; derive header bareness from fields

impl_bodies-nonempty was ONE FLAG ENCODING TWO FACTS: it stood in for "does
some field's type require this parameter to carry a bound", AND it was the
switch keeping the item header in agreement with its supplemental impl
headers. Removing it from the header site alone revealed the second fact
rather than breaking an invariant.

Both facts now derive from one authority. Header bareness reads
clone_bounded_type_params (the well-formedness fixpoint, "does NAMING G<A..>
require P: Clone"), which is exactly the field-derived property; the
GroupCompletion supplemental impls read the same record through one Bool.

Rust REQUIRES an impl's generics to satisfy the type's own bounds, so
struct S<M: Clone> with impl<M> Neg for S<M> cannot compile. The two headers
are ONE decision rendered twice, which makes single authority the construction
answer here, not a style preference. Note the two renderings are NOT the same
string: the bound belongs in the impl generics and must NOT appear in the type
reference (GroupCompletion<M: Clone> is not Rust).

Only a compiler fact crosses into extdeps.languages.rust.emit -- a Bool about
the carrier's declaration. The spelling (Clone, trait paths, where-clauses)
stays in extdeps, so the §3 duty is intact. Deliberately NOT routed through
spelling.where_bounds, which carries the OP's own requirement: merging them
would recreate the fused flag this change splits.

Precedent, same file: reference_use_lines_representation_invariant_note
resolved this class by deleting both halves of a fused flag and letting each
fact derive from its own authority.

ERROR TRAJECTORY -- the 21 were not a regression of the 1; they appeared ONLY
because the header fix was correct, and are resolved by feeding the impl
headers the same record:

  161  unconditional 'static
    1  after the 'static gate
   21  after the field-derived header rule (header fixed, impls still bare)
    0  after both headers read one authority

VERIFIED: first_generation_equal=true AND fixed_point_equal=true.

FILE OUTSIDE THIS LANE'S DECLARED PAIR: dag/extdeps/languages/rust/emit.dag
carries the impl header literal and has no sole-write owner; announced here so
anyone who considers it theirs sees it. Leaving the literal and matching it
compiler-side would have been a second representation of one fact.

KNOWN ASYMMETRY, named not fixed: the item header is rendered compiler-side
while the impl header is rendered in extdeps, so the tree answers "where does
a Rust bound get spelled" two ways.

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

* Name the has_fn_fields residue as a declared boundary with its trigger

Splitting the fused impl_bodies flag left has_fn_fields as the only thing
between the well-formedness record and the emitted header, which is exactly
where a second proxy would hide. Checked rather than asserted: it IS partly a
proxy.

It is a real discriminator for the DERIVE half -- a derive emits per-impl
bounds, so Monoid<T> { identity: T } with derive(Clone) gets
impl<T: Clone> Clone generated and needs no header bound. That half is correct
and is now stated as the reason, replacing a vague 'different carrier shape'.

It has NO authority over the WELL-FORMEDNESS half, which is independent of
derives entirely, yet it gates that computation too.

Population measured as a closed enumeration, not a spot check: 17 callable-
field generic structs (16 derive Clone); the complete non-callable, non-phantom
field set is &'a str, identity/zero/one/top/bottom: T, and
Rc<PartialFunction<K, V>>. Exactly one names a declared type, and
PartialFunction<K, V> is itself bare, so it carries no obligation. The exposing
population is EMPTY -- 'no instance today', NOT 'cannot occur'.

Deferred on displaced cost, NOT on unavailable evidence: a controlled fixture
plus a regen fixed point over the live 17 are both available today. With the
population empty the benefit is zero while the risk against 17 live carriers
is not, so it is worth less than what it displaces. Recording 'no evidence'
would have been false and would have retired on the wrong condition.

Mirrored defect class, worth stating together: 'static was a bound APPLIED to
a population that could not need it; this is a bound SUPPRESSED for a
population that does not yet exist. 'It cannot reject anything' and 'nothing
needs it' are the same unmeasured claim at opposite signs.

Annotation-only: regen fixed point unchanged (§4c -- annotations cannot alter
target-program bytes), verified rather than assumed.

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

* Update the witness consumers of the changed signature; add a two-position control

CI caught a real miss: rust_supplemental_impls_group_completion gained a
parameter and src/v2/test/claim/emit/trait_derive_seed_emit_binding_test.dag
still called it with zero arguments -- four call sites, four call-shape
refusals. I verified the emitter and the regen fixed point but not the WITNESS
consumers of the signature I changed. Local regen cannot catch this; only the
floor runs those tests.

Also adds the control that was missing for the failure mode most likely to
recur. Rust requires an impl's generics to satisfy the type's own bounds, so
the carrier bound is ONE FACT rendered TWICE, and the two renderings are not
the same string:

  impl<M: Clone> Neg for GroupCompletion<M>          compiles
  impl<M: Clone> Neg for GroupCompletion<M: Clone>   is not Rust

An implementation that renders one bounded value and substitutes it at both
positions passes a naive presence check and emits code rustc refuses, so the
negative assertion is the load-bearing half. The false arm additionally proves
the parameter DISCRIMINATES: without it, hardwiring the bound on would satisfy
every positive assertion.

VERIFIED BY EXECUTION, both directions, not by compiling:
  correct emitter                          -> returned true
  perturbed to bound the TYPE position too -> returned false
  restored                                 -> returned true

Also corrects a now-false sentence in trait_derive_emit_item_clone_bound_rule_
note, which still claimed arithmetic carriers with supplemental impl blocks
keep bare item params. They no longer do. Corrected in place rather than left
standing beside the change that falsified it.

Note the two prose edits behave differently, which is §4c working as specified:
the leading // annotation added earlier changed ZERO emitted bytes, while this
data ...: String row is program data and drifted its mirror. Prose in a String
is not free.

Regen: first_generation_equal=true AND fixed_point_equal=true.

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>
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