The Time generic is unreachable downstream — Untimed and custom timestamps cannot be used at all #68

Closed
opened 2026-09-09 17:53:12 +00:00 by logaritmisk · 0 comments
Owner

History<T: Time, ...> is generic over the time axis, Untimed is exported, and Time is a public trait documented as the extension point. A downstream crate cannot instantiate any T except i64.

Measured

All three routes fail from a consumer crate:

let _h: History<Untimed, ConstantDrift, NullObserver, &'static str> = History::builder().build();
// error[E0308]: expected `History<Untimed, ...>`, found `History<i64, ConstantDrift, ...>`

let _a: History<Untimed> = History::default();
// error[E0308]: same

let _b = HistoryBuilder::<Untimed, ConstantDrift, NullObserver, String>::default().build();
// error[E0599]: no associated function `default` found for `HistoryBuilder<T, D, O, K>`

Because:

  • History::builder() returns HistoryBuilder<i64, ...> (src/history.rs:348)
  • History::builder_with_key() also returns HistoryBuilder<i64, ...> (:356) — it generalises K only
  • HistoryBuilder's fields are private, it has no new(), and its only Default impl is for the i64 instantiation (:221)

HistoryBuilder has type-changing methods for D (drift) and O (observer), but none for T.

What that costs

  • Untimed is inert. Zero uses outside the crate's own unit tests. The README lists it as a shipped feature (a ticked Todo box).
  • Drift<T>'s generality is inert. The trait is generic over T specifically so "seasonal or calendar-aware drift is expressible without going through i64" — its own words. Nobody can write one.
  • A consumer with chrono::NaiveDate timestamps must convert to i64 and cannot express their domain type, which is the stated reason the parameter exists.

So one of History's four type parameters is paid for at every signature and cannot be varied. Either it earns its place or it should go.

Fix — two options

A. Make it reachable, consistent with how D and O already work:

impl<T, D, O, K> HistoryBuilder<T, D, O, K> {
    pub fn key_type<K2: Eq + Hash + Clone>(self) -> HistoryBuilder<T, D, O, K2>;
    pub fn time_type<T2: Time>(self) -> HistoryBuilder<T2, D, O, K>;
}

Then History::builder().time_type::<Untimed>().key_type::<String>().build() reads the same as .drift(..) and .observer(..), and builder_with_key can go — one entry point instead of a _with_key naming pattern that implies siblings it does not have.

Simpler alternative: a generic HistoryBuilder::new(), or impl Default for HistoryBuilder<T, ConstantDrift, NullObserver, K> generic in T.

B. Remove the generality. Delete Untimed, hard-code i64, drop T from every signature. That is a large simplification — but the README advertises the feature, and drift-over-real-time is the crate's premise, so (A) is the honest fix.

Either way this wants a doctest that actually constructs a non-i64 history, since nothing in the repo does and that is why it went unnoticed.

Breaks: (A) removes builder_with_key. (B) removes Untimed, Time's generality, and a type parameter.

Found by an API audit, 2026-09-09; all three failures reproduced independently.

`History<T: Time, ...>` is generic over the time axis, `Untimed` is exported, and `Time` is a public trait documented as the extension point. **A downstream crate cannot instantiate any `T` except `i64`.** ## Measured All three routes fail from a consumer crate: ```rust let _h: History<Untimed, ConstantDrift, NullObserver, &'static str> = History::builder().build(); // error[E0308]: expected `History<Untimed, ...>`, found `History<i64, ConstantDrift, ...>` let _a: History<Untimed> = History::default(); // error[E0308]: same let _b = HistoryBuilder::<Untimed, ConstantDrift, NullObserver, String>::default().build(); // error[E0599]: no associated function `default` found for `HistoryBuilder<T, D, O, K>` ``` Because: - `History::builder()` returns `HistoryBuilder<i64, ...>` (`src/history.rs:348`) - `History::builder_with_key()` also returns `HistoryBuilder<i64, ...>` (`:356`) — it generalises `K` only - `HistoryBuilder`'s fields are private, it has no `new()`, and its only `Default` impl is for the `i64` instantiation (`:221`) `HistoryBuilder` has type-changing methods for `D` (`drift`) and `O` (`observer`), but none for `T`. ## What that costs - **`Untimed` is inert.** Zero uses outside the crate's own unit tests. The README lists it as a shipped feature (a ticked Todo box). - **`Drift<T>`'s generality is inert.** The trait is generic over `T` specifically so "seasonal or calendar-aware drift is expressible without going through `i64`" — its own words. Nobody can write one. - A consumer with `chrono::NaiveDate` timestamps must convert to `i64` and cannot express their domain type, which is the stated reason the parameter exists. So one of `History`'s four type parameters is paid for at every signature and cannot be varied. Either it earns its place or it should go. ## Fix — two options **A. Make it reachable**, consistent with how `D` and `O` already work: ```rust impl<T, D, O, K> HistoryBuilder<T, D, O, K> { pub fn key_type<K2: Eq + Hash + Clone>(self) -> HistoryBuilder<T, D, O, K2>; pub fn time_type<T2: Time>(self) -> HistoryBuilder<T2, D, O, K>; } ``` Then `History::builder().time_type::<Untimed>().key_type::<String>().build()` reads the same as `.drift(..)` and `.observer(..)`, and `builder_with_key` can go — one entry point instead of a `_with_key` naming pattern that implies siblings it does not have. Simpler alternative: a generic `HistoryBuilder::new()`, or `impl Default for HistoryBuilder<T, ConstantDrift, NullObserver, K>` generic in `T`. **B. Remove the generality.** Delete `Untimed`, hard-code `i64`, drop `T` from every signature. That is a large simplification — but the README advertises the feature, and drift-over-real-time is the crate's premise, so (A) is the honest fix. Either way this wants a doctest that actually constructs a non-`i64` history, since nothing in the repo does and that is why it went unnoticed. **Breaks:** (A) removes `builder_with_key`. (B) removes `Untimed`, `Time`'s generality, and a type parameter. Found by an API audit, 2026-09-09; all three failures reproduced independently.
logaritmisk added the apibreakingbug labels 2026-09-09 17:57:57 +00:00
Sign in to join this conversation.