Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ All notable changes to Celerity are documented here. This project follows [Keep

### Fixed

- **`Deque<T>.Clear()` invalidated active enumerators even when the deque was already empty.** The version bump sat outside the guard that skips the array clearing, making `Deque` the only count-based collection in the library where a `Clear()` that removed nothing tore down live enumerators — and contradicting its own documented enumerator contract. A no-op `Clear()` is now a true no-op; clearing a populated deque still invalidates enumerators as before. Closes [#333](https://github.com/marius-bughiu/Celerity/issues/333).
- The rule above is now pinned family-wide by `ClearNoOpVersionTests` — one assertion per count-based collection, plus the two deliberate exceptions (`BitSet` and `FenwickTree` are fixed-length, so detecting "already empty" costs the same scan as the clear itself). Test-only; previously the rule was pinned per-collection for a handful of types, which is how the `Deque` outlier shipped. The testing guide and `CONTRIBUTING.md` now describe the family-wide suites and require a new collection to join them. Closes [#333](https://github.com/marius-bughiu/Celerity/issues/333).
- **A breaking API change could ship silently.** `dotnet pack` now validates every package against its last published version across all three TFMs and fails on any break, so a removed or narrowed public member can no longer reach NuGet.org with CI green. It runs on every PR, not just at release, and intentional breaks are recorded in a reviewed suppression file. CI-only; no consumer-visible behaviour change. Closes [#315](https://github.com/marius-bughiu/Celerity/issues/315).
- **A bad `CHANGELOG.md` could half-publish a release.** The release-notes check ran after the irreversible NuGet push, so a missing section — or one over GitHub's ~125k release-body cap, which this repo has overrun before — left six packages published and no release. It now runs before anything is pushed, with a body-size assertion added, and the validated notes are handed to the release step verbatim. Closes [#315](https://github.com/marius-bughiu/Celerity/issues/315).
- **The `EnumMap` and `EnumSet` cards on the benchmark dashboard rendered empty.** The page required an `(ItemCount: N)` suffix on every result name, and both benchmarks deliberately declare no item-count sweep, so their measurements were published and then discarded at render time. Both cards now chart their real numbers, and unparameterized benchmarks are excluded from the headline speedup stats. Closes [#301](https://github.com/marius-bughiu/Celerity/issues/301).
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ These are enforced by review, not by an analyzer. Reading the existing code is t
- Prefer `[Fact]` for a single case, `[Theory] + [InlineData]` for parameterized cases.
- When fixing a bug, add a test that fails on `main` and passes on your branch. It's fine to reference the issue number in a comment.
- New collections are expected to carry parity coverage at every layer: behavioural tests, a CsCheck property test against the closest BCL oracle, and a `Celerity.Fuzz` target. See the [Testing & coverage guide](docs/testing.md) for how each layer works and how to run them.
- A new collection must also be added to the **cross-collection suites** that assert one rule across the whole family, not only to its own `*Tests.cs`. `grep` `src/Celerity.Tests/Collections/` for them before assuming you have found them all; `ClearNoOpVersionTests.cs` (a `Clear()` that removes nothing must not bump the version) is the one that applies to every count-based collection. A type that is absent from these suites is not covered for the invariants the rest of the family guarantees.
- Coverage is gated in CI (`.github/workflows/coverage.yml`) at **100% line and 100% branch**, across all six shipping packages. New code arrives with its tests, or the gate goes red. If you hit a branch no test can reach, exclude it at the source with `[ExcludeFromCodeCoverage(Justification = "…")]` explaining why — do not lower the floor. See the [Testing & coverage guide](docs/testing.md) for the current exclusions and the reasoning behind each.
- Adding a new shipping package? Add its assembly to `src/coverage.runsettings` and its test project to the coverage workflow. Coverlet's assembly filter is exact-match, so an unlisted package is silently unmeasured.

Expand Down
1 change: 1 addition & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ The 2026-Q3 review surveyed the shipped surface against the current .NET 8/9/10
- Fix `HyperLogLog`'s hash-entropy floor. `Hash64` widened a 32-bit `IHashProvider<T>` result, so the reachable hash space was 2^32 — while the type's own docs asserted a 64-bit space and skipped the classical large-range correction on that basis. The bias exceeded the advertised 0.81% standard error from ~1e8 distinct elements, in exactly the regime the type is sold for. Status: `done` — `IHashProvider64<T>` (`ulong Hash64(T key)`) ships as a standalone sibling interface in `Celerity.Hashing`, deliberately *not* deriving from `IHashProvider<T>` so the two contracts stay independent and a 64-bit hasher is never forced to publish a lossy 32-bit fold. Fourteen built-in hashers implement it — `Int64WangHasher`, `Int64Murmur3Hasher`, `UInt64WangHasher`, `UInt64Hasher`, `GuidHasher`, and the nine 64-bit `string` hashers — each of which already computed 64 bits internally and folded them away, so `Hash64` is the same mixer minus the narrowing. The 32-bit-only hashers (`Int32*` / `UInt32*`, the naive folds, `DefaultHasher<T>`) deliberately do not, since a key type narrower than 64 bits has no entropy to publish; a roster test pins that judgement. All five sketches route through it when the hasher provides it, via a compile-time type test the JIT folds away (so neither path allocates or branches) and with existing constructors and type parameters unchanged; on a 32-bit hasher `HyperLogLog` now applies the classical Flajolet large-range correction it previously skipped. `HashQualityEvaluator.Evaluate64` reports distribution over the 64-bit surface. Tracked in [#304](https://github.com/marius-bughiu/Celerity/issues/304).
- Implement `IReadOnlySet<T>` on the mutable sets and `IDictionary<TKey, TValue>` on the dictionaries. The sets implement `ISet<T>` and the dictionaries `IReadOnlyDictionary<,>`, but `ISet<T>` does not derive from `IReadOnlySet<T>` — so an ordinary BCL-shaped API taking either interface is a compile error against a Celerity type today. This is the same Guiding Principle #3 gap the 2.2.0 set-algebra work closed, one level up. Status: `planned`.
- Delete the per-probe virtual call. The probe loops test for an empty slot with `EqualityComparer<TKey>.Default.Equals(slot, default(TKey))`, which the JIT devirtualizes for value-type keys but not under `__Canon`-shared reference-type instantiations — one `callvirt` per probe iteration to perform what is a null check. Guiding Principle #2 exists to remove exactly this. Status: `done` — the twelve open-addressed collections now route every vacant-slot test through an internal `EmptySlot.Is<T>` helper whose `typeof(T).IsValueType` guard the JIT folds, so a reference-type instantiation compiles to a plain null test and a value-type one keeps the existing intrinsic comparison unchanged. Behaviour is identical by construction and the whole existing suite passes untouched; `ReferenceKeyProbeTests` pins the substitution against a key type whose `Equals` claims equality with `null`, and the new `StringKeyProbeBenchmark` gives the dashboard its first reference-type-key rows. The follow-up `IEqualityProvider<T>` idea was **not** opened: a `HashCachingDictionary` control arm showed the residual reference-type-key deficit is dominated by re-hashing the key on every probe, not by the remaining equality dispatch — the actionable guidance is to use the hash-caching variants, now documented in [`docs/performance.md`](docs/performance.md#reference-type-keys-cache-the-hash). Tracked in [#308](https://github.com/marius-bughiu/Celerity/issues/308).
- Restore the family-wide no-op-`Clear()` contract. The library is otherwise strict that an operation which changes nothing observable does not invalidate enumerators — `FenwickTree` documents it for a zero delta, `BTreeDictionary` for a rejected duplicate `TryAdd`, `LruCache` for a hit on the already-MRU entry — but `Deque<T>` bumped its version outside the guard that skips the array clearing, so clearing an already-empty deque tore down every live enumerator, contradicting `Deque`'s own documented contract. Status: `done` — the bump moved inside the guard (Option A of the issue: match Celerity's own family, since the BCL points both ways — `Dictionary<K,V>.Clear()` bumps only when non-empty while `Queue<T>` / `Stack<T>` bump unconditionally). The rule is now pinned once per collection by the new family-wide `ClearNoOpVersionTests`, which also pins the two deliberate exceptions: `BitSet` and `FenwickTree` are fixed-length, so establishing "already empty" costs the same scan as the unconditional clear it would skip, and they agree with each other. Tracked in [#333](https://github.com/marius-bughiu/Celerity/issues/333).
- Span-keyed lookups on the string-keyed collections. .NET 9's `GetAlternateLookup<ReadOnlySpan<char>>` lets the BCL `Dictionary` probe with a span key and no allocation; Celerity's string-keyed types require a materialized `string`, so the BCL is now *ahead* on the axis this library has invested most in. Status: `done` — `ISpanHashProvider` (`int Hash(ReadOnlySpan<char> key)`) ships as a standalone sibling interface in `Celerity.Hashing`, deliberately *not* deriving from `IHashProvider<T>`: that interface is generic in its key type, and a `ref struct` could not be a generic type argument before `allows ref struct` (C# 13 / .NET 9) while `net8.0` remains the floor — expressing the span overload as a non-generic sibling sidesteps that, because the span is a method parameter rather than a type argument. All 23 built-in `String*Hasher` types implement it, each sharing one body between the two overloads so they cannot drift; `SpanHashParityTests` pins `Hash(s) == Hash(s.AsSpan())` per hasher across every length class and as a slice of a larger buffer, since a divergence would silently report a stored key as absent rather than merely being slow. `FrozenCelerityDictionary`, `FrozenCeleritySet`, `CelerityDictionary<string, …>`, `CeleritySet<string, …>` and `Trie<TValue>` gained span `TryGetValue` / `ContainsKey` / `Contains`; on the four hashed types they are extension methods carrying the extra `ISpanHashProvider` constraint on the *method*, so no shipped type's own constraints changed (which would have broken every existing instantiation) and the JIT still devirtualizes through the struct type parameter. `StringInternTable` ships alongside them as the type the pattern makes possible: `GetOrAdd(ReadOnlySpan<char>)` allocates only on a miss, so a 10M-cell parse over 100 distinct tokens creates 100 strings — the one collection the pre-.NET-9 BCL cannot express, since `HashSet<string>.TryGetValue` makes you allocate the string before you can discover you already had it. The optional `ReadOnlySpan<byte>` UTF-8 axis and the `#if NET9_0_OR_GREATER` `IAlternateEqualityComparer` plumbing were both left out as the issue's own scoping allowed — neither is needed for the workload win, and each would widen a brand-new public abstraction before it is load-bearing. Tracked in [#311](https://github.com/marius-bughiu/Celerity/issues/311).

**The ordered / compressed integer-data lane.** Opened by the sorted-container hole — 38 collections and not one sorted map or set, with `Trie` the only ordered type, and that one string-keyed. The B-trees below close that half; the compressed-integer half is still open.
Expand Down
3 changes: 2 additions & 1 deletion docs/api/collections.md
Original file line number Diff line number Diff line change
Expand Up @@ -3502,7 +3502,8 @@ public Deque(IEnumerable<T> collection)
- `void TrimExcess()` — shrinks the backing array to exactly `Count`, re-linearizing so the front
sits at index `0`.
- `void Clear()` — removes all elements; the backing array is retained (use `TrimExcess` to release
it).
it). Clearing an **already-empty** deque is a true no-op and does **not** invalidate an active
enumerator, matching the rest of the collection family.
- `Enumerator GetEnumerator()` — an allocation-free struct enumerator yielding elements **front to
back**; a structural modification during enumeration throws `InvalidOperationException`.

Expand Down
1 change: 1 addition & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ The bulk of the suite lives in `Celerity.Tests`, mirroring the library's folder
- **Collision tests** (`*CollisionTests.cs`) — force every key down one probe chain with a constant hasher, then verify lookups, removals, and backward-shift deletion keep every entry findable.
- **Enumeration tests** (`*EnumerationTests.cs`) — the struct enumerators, `Keys`/`Values` views, mid-enumeration mutation detection, and the non-generic interface surface (`IEnumerable.GetEnumerator()`, `object IEnumerator.Current`, `IEnumerator.Reset()`).
- **Load-factor / constructor validation** — boundary resizes and argument checking.
- **Family-wide invariant suites** — a single file asserting one rule once per collection, so a new type (or an edit to an existing one) cannot quietly drift out of the family. `ClearNoOpVersionTests.cs` is the model: it pins *a `Clear()` that removes nothing does not bump the version*, so a defensive clear leaves active enumerators valid, across every count-based collection — and pins the two deliberate exceptions (`BitSet` and `FenwickTree` are fixed-length, so establishing "already empty" costs the same scan as the clear) so they read as decisions rather than as oversights. `Deque<T>` shipped as the one outlier precisely because this rule was only pinned per-collection beforehand.
- **Edge cases** live next to the type they exercise rather than in a catch-all file: indexer misses on the out-of-band key and `Clear()` on an empty collection sit in `*Tests.cs`; the wrap-around cluster that exercises the `bypassesGap` branch of backward-shift deletion sits in `*CollisionTests.cs`.

Run them with:
Expand Down
Loading
Loading