Skip to content

applied: DecoratedCospan's derived Clone/Debug bound D, not D::Apex — uncloneable for every decoration that exists #348

Description

@tsondru

Found during #289 (G2-T3), r6 application, 2026-08-22 — while hand-writing PartialEq for DecoratedCospan. The PartialEq case was fixed there; Clone and Debug carry the identical defect and were left alone as out of scope.

The defect

catgraph-applied/src/decorated_cospan.rs:137:

#[derive(Clone, Debug)]
pub struct DecoratedCospan<Lambda, D>
where
    Lambda: Eq + Copy + Debug,
    D: Decoration,
{
    pub cospan: Cospan<Lambda>,
    pub decoration: D::Apex,   // <-- D::Apex, not D
}

derive bounds the type parameters, not the field types. So it generates impl<Lambda, D> Clone for DecoratedCospan<Lambda, D> where Lambda: Clone, D: Clone, requiring D: Clone — but D is a marker that is never stored. The field is D::Apex.

No Decoration marker in the workspace implements Clone:

marker derives visibility
PetriDecoration (petri_net.rs:613) Debug pub
Trivial (decorated_cospan.rs:371) Debug test-module-private
Counter (decorated_cospan.rs:457) Debug test-module-private
Circuit (tests/, examples/) none local
LocalTrivial (tests/decorated_cospan.rs:98) none local

So DecoratedCospan is uncloneable for every decoration that exists — including PetriDecoration, the only one a downstream consumer can name. Measured on audit/G2-T3: cloning a DecoratedCospan<char, Trivial> gives E0599, "the method clone exists for struct DecoratedCospan<char, Trivial>, but its trait bounds were not satisfied … Trivial: Clone is not satisfied".

Debug has the same shape but bites less: PetriDecoration, Trivial and Counter do derive Debug, so it only fails for Circuit / LocalTrivial.

Why this is the same bug #289 already fixed once

#289's r6 pass added PartialEq to this type and hit exactly this. A #[derive(PartialEq)] there compiles but applies to no decoration in the workspace — measured, three E0369s. The fix that shipped was a hand-written impl bounded on D: Decoration and D::Apex: PartialEq rather than D: PartialEq. Clone and Debug need the same treatment and did not get it.

Work

Replace #[derive(Clone, Debug)] with hand-written impls asking only what is stored:

impl<Lambda, D> Clone for DecoratedCospan<Lambda, D>
where
    Lambda: Eq + Copy + Debug,
    D: Decoration,
    D::Apex: Clone,
{ /* ... */ }

Mirror for Debug. Follow the PartialEq impl that shipped in #289 — it is the worked example, in the same file.

Falsify: add a test that clones a DecoratedCospan<char, Trivial> (and Debug-formats a DecoratedCospan<_, Circuit>). Confirm it fails to compile against the derives before the fix — this is a trait-bound defect, so the falsification is a compile failure, not a red assertion — then passes after. Record the E0599 text in the commit.

Consider while there: whether the markers should simply derive Clone/Copy/Debug as unit structs. That also fixes the symptom and is a one-line change per marker, but it leaves the wrong bound in place, so the next marker anyone adds reintroduces it. The impls are the fix; deriving on the markers is a workaround.

Severity

Minor-to-important. No in-tree caller clones a DecoratedCospan, so nothing is broken today — but it is a public generic type that cannot be cloned for any decoration that exists, and the failure lands on the consumer as a confusing E0599 about a bound they never wrote. Group: applied; no dependency on the audit sweep's remaining rows.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    audit-sweep2026-08 audit sweep: paper/test-layer audit findings

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions