--- name: support-3rdparty-traits description: This skill should be used when the user asks to "support a trait from another crate", "implement a third-party trait", "add an integration for crate X", "add num-traits / serde / rand / ... support", "add a feature flag for a new dependency", "wire up crate X behind a feature", "expose type X for crate Y", or "support a new major version of rand / serde / num-traits / diesel / postgres-types" (e.g. "add rand_v09 alongside rand_v08", "support diesel v3"). It provides the SOP for adding an optional third-party dependency behind the project's versioned-feature-flag policy, placing the trait impls in the right module, forwarding the feature across crates and the meta crate, and keeping the no_std/MSRV/changelog invariants intact. --- # Supporting Third-Party Traits in dashu Standard operating procedure for two related changes: - **Branch A** — implement traits from a *new* third-party crate (e.g. add `borsh`, `arbitrary`, `bytemuck` support, or a trait crate the repo doesn't depend on yet). - **Branch B** — support a *new major version* of a crate that is already integrated (e.g. add `rand_v09` alongside `rand_v08`, or `diesel_v3` alongside `diesel_v2`). Both share the same invariants; Branch B reuses Branch A's steps plus a versioning layer. ## Repository context (load-bearing facts) Read these before touching anything — they are the conventions every existing integration follows. - **Impls live in `/src/third_party/`**, one file per external crate. `/src/third_party/mod.rs` gates each submodule with `#[cfg(feature = "...")]`. `/src/lib.rs` declares `mod third_party;` (private) then `pub use third_party::*;` to expose any public items. - **Two visibility flavors in `mod.rs`:** - `pub mod rand;` — when the file defines *new public types* (e.g. `UniformBits`, `UniformUBig`) that users import from `crate::rand`. - `mod num_traits;` / `mod serde;` — when the file only `impl`s an external trait for a local type; the impl is globally reachable once compiled, so nothing public needs re-exporting. - **"Stable" vs "unstable" dependencies** (this split is explicit in every `Cargo.toml`, marked `# stable dependencies` / `# unstable dependencies`): - *Stable* = the crate's API is essentially frozen across versions (serde, zeroize, num-order). One feature, named after the crate. Example: `serde = ["dep:serde", "dashu-int/serde"]`. - *Unstable* = breaking changes between major versions (rand, num-traits, num-integer, diesel, postgres-types). Always a **versioned** feature/dep with a `_vXX` suffix, plus an **unversioned alias** feature pointing at the current default. - **Versioned dependency = renamed optional dep.** The dep *key* carries the suffix and `package =` resolves it to the real crate: ```toml rand_v08 = { optional = true, version = "0.8.3", package = "rand", default-features = false } ``` In Rust code you import by the suffixed key: `use rand_v08::{Rng, ...};`. Inside a file you may alias it back to the natural name for readability: `use num_traits_v02 as num_traits;`. - **Versioned feature enables its own dep AND forwards downstream.** Because float/rational re-export int's types, their feature must also turn on the matching feature in the lower crate: ```toml num-traits_v02 = ["dep:num-traits_v02", "dashu-int/num-traits_v02"] ``` - **Unversioned alias** points at the latest version: `rand = ["rand_v08"]`, `num-traits = ["num-traits_v02"]`. Users write `features = ["rand"]`; the alias decides which real version that means. - **Multiple major versions coexist as peers** (see diesel): `diesel_v1` and `diesel_v2` are both real, each with its own versioned dep + feature + module file (`third_party/postgres/diesel_v1.rs`, `diesel_v2.rs`). Shared conversion logic lives in `third_party/postgres/mod.rs`; each version file only wires that logic to its own diesel traits. - **Private helper deps** use a leading-underscore key and are never given their own user-facing feature. Enabled as a side effect: `num-order = ["dep:num-order", "dep:_num-modular"]` (rational). - **no_std:** every optional dep is declared `default-features = false` so it cannot drag `std` into the default (no_std) build. If the dep itself needs std, the feature must pull in `std` (e.g. `postgres-types_v02 = ["dep:postgres-types_v02", "dep:_bytes", "std"]`). - **Meta crate `dashu` (root `Cargo.toml`) forwards every feature** to all sub-crates that have it — both the versioned form and the unversioned alias: `rand_v08 = ["dashu-int/rand_v08", "dashu-float/rand_v08", "dashu-ratio/rand_v08"]` and `rand = ["dashu-int/rand", ...]`. - **MSRV is 1.68.** A new dep must build on 1.68; if it doesn't, it must be stripped from MSRV builds via `.github/workflows/drop_incompatible_deps_for_msrv.py` (see pre-publish-check skill, Step 6). - **Two `Cargo.toml` styles coexist in the repo:** `integer` uses the *legacy* style where `optional = true` deps auto-create an implicit feature (so `serde`/`zeroize`/`num-order` aren't listed in `[features]`). `float`/`rational` use the *modern* explicit style with `dep:` syntax. **Prefer the `dep:` (modern) style** for any new work; match the surrounding crate's existing style when editing an existing feature. ## Decision: which branch? - The crate is **not yet** in any `Cargo.toml` → **Branch A**. - The crate **is** already integrated and you're adding a second major version (or replacing the supported one) → **Branch B**. --- ## Branch A — Support a trait from a new third-party crate ### Step A1 — Classify the dependency: stable or unstable? If the upstream crate has a history of breaking changes between major versions (rand, num-traits, num-integer) or is pre-1.0, treat it as **unstable** → you need the versioned suffix machinery. If its API is effectively frozen at 1.x (serde, zeroize, num-order) → **stable** → a single unversioned feature suffices. When in doubt, choose unstable; the cost is one extra alias line and you avoid a painful migration later. ### Step A2 — Declare the optional dependency in the target crate's `Cargo.toml` Under the matching `# stable dependencies` or `# unstable dependencies` comment. Always `default-features = false`. Stable: ```toml borsh = { optional = true, version = "1.x", default-features = false } ``` Unstable (note the `_vXX` key + `package =`): ```toml arbitrary_v1 = { optional = true, version = "1.x", package = "arbitrary", default-features = false } ``` ### Step A3 — Define the feature flag(s) Stable (modern `dep:` style): ```toml borsh = ["dep:borsh"] ``` Unstable — two entries: the versioned feature (with cross-crate forwarding if a lower crate also exposes the type) plus the unversioned alias: ```toml arbitrary_v1 = ["dep:arbitrary_v1"] arbitrary = ["arbitrary_v1"] ``` If the trait must also be enabled on a crate this one depends on (because it re-exports that crate's types), add the downstream feature to the versioned feature list — see load-bearing fact above. ### Step A4 — Create the implementation module Add `/src/third_party/.rs`. Import the external crate by its dep key (alias back to the natural name if it reads better). `impl` the external trait(s) for the local types (`UBig`/`IBig`/`FBig`/`RBig`). Follow existing files as a template: `integer/src/third_party/num_traits.rs` (pure trait impls), `integer/src/third_party/rand.rs` (defines new public types + a module-level `# Examples` doc), `float/src/third_party/postgres/` (multi-version, shared logic). If the file defines new public items the user will name, declare it `pub mod` in `mod.rs`; otherwise plain `mod`. ### Step A5 — Wire it into `third_party/mod.rs` ```rust #[cfg(feature = "borsh")] mod borsh; // or, if it exposes public types: #[cfg(feature = "arbitrary_v1")] pub mod arbitrary; ``` No change to `lib.rs` is needed beyond the existing `pub use third_party::*;` — that line already re-exports anything `pub` from these modules. ### Step A6 — Forward the feature across crates (if applicable) If the same trait makes sense on types from more than one sub-crate, repeat Steps A2–A5 in each crate, and make each higher crate's versioned feature turn on the lower crate's matching feature (load-bearing fact). Keep the feature *name* identical across crates (`arbitrary_v1` everywhere). ### Step A7 — Forward the feature in the meta crate In the root `Cargo.toml` `[features]`, add both forms (versioned + unversioned alias), each forwarding to every sub-crate that implements it: ```toml arbitrary_v1 = ["dashu-int/arbitrary_v1", "dashu-float/arbitrary_v1", "dashu-ratio/arbitrary_v1"] arbitrary = ["dashu-int/arbitrary", "dashu-float/arbitrary", "dashu-ratio/arbitrary"] ``` List only the sub-crates that actually have the feature — do not forward to a crate that doesn't implement it (that's a build error). ### Step A8 — Tests, doc examples, dev-dependencies - Add a `[[test]]` with `required-features = [""]` (see the `random` / `serde` tests in each `Cargo.toml`), or put unit tests in a `#[cfg(test)] mod tests` at the bottom of the new module file. - Re-declare the dep (un-suffixed or suffixed to match) under `[dev-dependencies]` *without* `optional`, so doctests/examples resolve. - Any public item documented at module level (like `rand.rs`) needs a `# Examples` block; in examples `use` the dep by its dep key (`use rand_v08::{...}`). ### Step A9 — MSRV check Confirm the new dep's `rust-version` (or last-known-compatible release) is ≤ 1.68. If not, add it to `drop_incompatible_deps_for_msrv.py` so MSRV CI builds strip it. ### Step A10 — Changelog Add an entry under `### Add` in the `## Unreleased` section of *every* crate whose `Cargo.toml` you changed (target crate, any crate that got cross-crate forwarding, and note the meta crate has no changelog). New optional deps are additive → patch bump. --- ## Branch B — Support a new major version of an existing crate Example below uses adding `rand_v09` alongside `rand_v08`; substitute names as needed. ### Step B1 — Add the new versioned dependency Keep the old one; add the new one beside it under `# unstable dependencies`: ```toml rand_v08 = { optional = true, version = "0.8.3", package = "rand", default-features = false } rand_v09 = { optional = true, version = "0.9.0", package = "rand", default-features = false } ``` Both keys resolve to the real `rand` crate via `package =`. They coexist fine because the keys differ. ### Step B2 — Add the versioned feature In every crate that supports it, mirroring the old version's feature line and its cross-crate forwarding: ```toml rand_v09 = ["dep:rand_v09", "dashu-int/rand_v09"] # in float/rational rand_v09 = ["dep:rand_v09"] # in integer ``` ### Step B3 — Decide the unversioned alias target The alias `rand = [...]` selects the default version users get. Two options: - **Keep the alias on the old version** (`rand = ["rand_v08"]`) — purely additive, no behavior change for existing `features = ["rand"]` users. **Recommended default.** - **Move the alias to the new version** (`rand = ["rand_v09"]`) — existing users silently upgrade. This is a *breaking* change in effect; only do it deliberately, call it out in the changelog under `### Change`, and require at least a minor bump (pre-1.0 rule). If you move the alias, the old versioned feature still exists for users who need to pin it explicitly. ### Step B4 — Create/adapt the implementation module Add `/src/third_party/rand_v09.rs` (or whatever splits cleanly). Often you can share logic with the old-version file by extracting the version-independent core into a helper (the `postgres/mod.rs` `Numeric` struct + conversion fns are the model: shared core, version-specific trait wiring in `diesel_v1.rs`/`diesel_v2.rs`). Gate the new module in `mod.rs`: ```rust #[cfg(feature = "rand_v08")] pub mod rand; // existing #[cfg(feature = "rand_v09")] pub mod rand_v09; // new ``` Port every trait impl from the old file, adjusting for API differences in the new major version. ### Step B5 — Cross-crate + meta forwarding Repeat Step B2 in every implementing crate, and add `rand_v09 = [...]` (and, if you moved the alias, the updated `rand = [...]`) to the root `Cargo.toml` forwarding to all sub-crates that have it. ### Step B6 — Tests, doc examples, dev-dependencies Add a dev-dependency for the new version, and either a new `[[test]]` gated on `rand_v09` or extend the existing one to cover both versions. Mirror the old version's doctest coverage using the new dep key. ### Step B7 — Changelog Under `### Add` in each affected crate's `## Unreleased`: "Add support for rand 0.9 (behind the `rand_v09` feature)." If you moved the unversioned alias (Step B3 option 2), *also* add a `### Change` entry and treat the release as breaking. --- ## Verification After either branch, confirm the feature compiles in isolation and that the no_std path still holds: ```sh # feature on, 64-bit Word cargo check -p --features # feature on, 32-bit Word (NTT and some paths are 32-bit-specific) RUSTFLAGS='--cfg force_bits="32"' cargo check -p --features # no_std still builds (the new dep must not have pulled in std) cargo build -p --no-default-features --features # clippy clean with the feature on cargo clippy -p --features -- -D warnings # doctests/examples for the new module cargo test -p --features --doc # meta crate forwards correctly cargo check -p dashu --features ``` If the feature exists in multiple sub-crates, run the `--features` check on each and on `-p dashu`. ## Common failure modes - **Forgot the `package =` rename on a versioned dep** — `rand_v09` without `package = "rand"` makes cargo look for a (nonexistent) crate literally named `rand_v09`. Every versioned dep needs `package = ""`. - **Forgot cross-crate forwarding** — float/rational's `num-traits_v02` feature must list `dashu-int/num-traits_v02`; without it, enabling the feature on the higher crate doesn't give the lower crate's types the impl. - **Forwarded a feature to a crate that doesn't have it** — `dashu-base` has no `rand` feature; listing it in the meta crate's `rand` forwarding is a build error. Only forward to crates that actually define the feature. - **Dep without `default-features = false`** — silently breaks the no_std build (serde/zeroize pull std by default). Every optional dep in this repo sets it. - **`#[cfg]` feature name ≠ Cargo feature name** — `mod.rs` gates on the *feature* name (`rand_v08`), not the dep key's package. They usually match the key, but if you renamed via `package =`, the cfg still uses the key/feature name, not `rand`. - **Added an unstable dep without the unversioned alias** — users expect `features = ["rand"]` to work; a versioned-only feature breaks that contract. - **New dep exceeds MSRV** — builds fine on stable CI, fails the `1.68` matrix job. Must be stripped in `drop_incompatible_deps_for_msrv.py`. - **Visibility mismatch** — declared `mod foo` in `mod.rs` but the module defines public types the user needs (`crate::foo::UniformBits`). If users name items from the module, it must be `pub mod`. - **Missed a changelog** — every `Cargo.toml` you edited (target crate + forwarded crates) needs an `## Unreleased` entry; the meta crate has none.