Skip to content

v1.1.2

Choose a tag to compare

@github-actions github-actions released this 01 May 14:24
· 561 commits to main since this release

First successful 1.1.x publish. Tags v1.1.0 and v1.1.1 exist on the repository but never published to nuget.org: v1.1.0 failed at deploy with HTTP 403 (NuGet API key had expired), and the follow-up v1.1.1 failed with HTTP 401 because the trusted-publishing migration used the wrong NuGet account name (marius-bughiu instead of marius.bughiu). 1.1.2 is the same library code as the 1.1.0 tag plus the trusted-publishing migration with the correct user, shipped under a fresh version because the failed tags couldn't be cleanly recycled.

Changed

  • CS1591 (missing XML doc comment) is now a build error in Celerity.csproj, not just a warning. Celerity ships its generated .xml documentation file with the NuGet package; gating on CS1591 ensures every public type / member retains a doc comment so that file is never silently incomplete. The library was already at 100% public-symbol doc coverage at the time of the change, so no source had to be updated — this is purely a guardrail to prevent regression. Implements the "Bump XML doc coverage; treat missing docs as warning-as-error" item from the milestone 1.1.0 infrastructure roadmap. Scoped to the main library .csproj only; the test and benchmark projects are unaffected because they do not set <GenerateDocumentationFile>true</GenerateDocumentationFile>.

Added

  • Cross-platform CI matrix — .github/workflows/ci.yml now runs dotnet build and dotnet test on ubuntu-latest, windows-latest, and macos-latest for every push to main and every pull request, with fail-fast: false so a regression on one OS does not mask the result on the others. Test-result artifacts are uploaded per-OS (test-results-<os>) to avoid name collisions. Closes the cross-platform testing item on milestone 1.1.0 (#28).
  • Remove(TKey key, out TValue? value) BCL-parity overload on both CelerityDictionary<TKey, TValue, THasher> and IntDictionary<TValue, THasher>. The captured value is the value that was associated with the key immediately before removal, and is default(TValue) when the key was not found. The out-of-band default-key / zero-key slot is surfaced through the same path. The existing void Remove(key) overload now delegates to this method, so the rehash-after-remove path is unchanged and continues to bump the dictionary's _version counter so active enumerators throw InvalidOperationException on the next MoveNext. Closes issue #23.
  • RemoveOutValueTests — coverage for both dictionaries: standard-key capture, missing-key returning false and default, out-of-band default-key / zero-key / null-string-key capture, rehash-after-remove of an interior cluster element under forced full-collision, remove-then-reinsert, default-value-type capture (guards against conflating the captured value with the empty-slot sentinel), enumerator invalidation on the new overload, and a regression check that the void Remove overload still returns true after the delegation refactor.
  • LongDictionary<TValue, THasher> and the LongDictionary<TValue> convenience subclass — high-performance dictionary keyed by long, mirroring the IntDictionary surface for 64-bit keys. Defaults to Int64WangHasher. Implements IReadOnlyDictionary<long, TValue?>, ships allocation-free struct enumerator and KeyCollection / ValueCollection views, the IEnumerable<KeyValuePair<long, TValue>> constructor, full Add / TryAdd / TryGetValue / Clear surface, and constructor validation matching IntDictionary. The zero key (0L) collides with the EMPTY_KEY sentinel exactly as on IntDictionary and is stored out-of-band via a dedicated flag + value slot, so map[0L] = x is round-trippable. Closes the last collection-shape gap in milestone 1.1.0.
  • LongDictionaryTests — mirrors the IntDictionaryTests coverage (CRUD, zero-key, resize survival, remove-then-reinsert, constructor validation, IEnumerable constructor, Add / TryAdd duplicate semantics, struct-enumerator and Keys / Values views, and the boxed IReadOnlyDictionary<long, TValue?> surface) plus long-specific cases: extreme key values (long.MaxValue, long.MinValue, int.MaxValue + 1L, int.MinValue - 1L, -1L) and a regression test that two keys sharing the same lower 32 bits but differing in the upper 32 bits are kept distinct (guards against any accidental int-truncation on the probe path).
  • CeleritySet.GetEnumerator() and IEnumerable<T> conformance — struct-based, allocation-free enumeration over a CeleritySet<T, THasher>. The out-of-band default(T) entry (zero for primitives, Guid.Empty for Guid, null for reference-type elements) is yielded first; the rest of the elements follow in unspecified order. The struct enumerator tracks a _version counter (bumped on every entry-point structural mutation: Add, TryAdd, Remove, Clear) and throws InvalidOperationException on MoveNext / Reset if the set is mutated mid-enumeration, matching BCL HashSet<T> semantics. Closes the second slice of issue #23 (IntSet shipped in PR #50); the sets are now at full enumeration parity with the dictionaries. Unblocks the post-1.1.0 IReadOnlySet<T> interface and an IEnumerable<T> constructor mirroring dictionary issue #22.
  • CeleritySetEnumerationTests — 26 tests mirroring IntSetEnumerationTests, covering empty / single / many-entry enumeration, default-value-first ordering for both value-type and reference-type (null) elements, removal / default-removal / Clear survival, multi-resize survival from a tiny initial capacity, mutation-during-enumeration detection on insert / default-insert / remove / default-remove / clear, Reset invalidation under mutation, no-op mutations (re-adding an existing item, re-adding a present default item, removing an absent item, Clear on an empty set) preserving enumerator validity, Reset reusability, post-exhaustion Current reset, generic and non-generic IEnumerable parity, CeleritySet<int, Int32Murmur3Hasher> open-generic coverage, and a LINQ smoke test (Count / Sum / Contains) confirming the boxed enumerator path.
  • IntSet.GetEnumerator() and IEnumerable<int> conformance — struct-based, allocation-free enumeration over an IntSet<THasher>. The out-of-band zero entry is yielded first; the rest of the elements follow in unspecified order. The struct enumerator tracks a _version counter (bumped on every entry-point structural mutation: Add, TryAdd, Remove, Clear) and throws InvalidOperationException on MoveNext / Reset if the set is mutated mid-enumeration, matching BCL HashSet<T> semantics. The dictionaries already shipped this surface (issue #10); this closes the symmetric gap on the sets and is the first slice of issue #23 (CeleritySet follows in a separate PR). Unblocks the IEnumerable<int> constructor and the post-1.1.0 IReadOnlySet<int> interface.
  • IntSetEnumerationTests — 25 tests covering empty / single / many-entry enumeration, zero-first-when-present ordering, removal / zero-removal / Clear survival, multi-resize survival from a tiny initial capacity, mutation-during-enumeration detection on insert / zero-insert / remove / zero-remove / clear, Reset invalidation under mutation, no-op mutations (re-adding an existing item, removing an absent item, Clear on an empty set) preserving enumerator validity, Reset reusability, post-exhaustion Current reset, generic and non-generic IEnumerable parity, IntSet<Int32Murmur3Hasher> open-generic coverage, and a LINQ smoke test (Count / Sum / Contains) confirming the boxed enumerator path.
  • IEnumerable<KeyValuePair<TKey, TValue>> constructor on both CelerityDictionary<TKey, TValue, THasher> and IntDictionary<TValue, THasher> (and the IntDictionary<TValue> convenience subclass). Matches BCL Dictionary<,> semantics: throws ArgumentNullException on a null source and ArgumentException on duplicate keys (including duplicate zero / default keys). When the source implements ICollection<T>, its Count is used to size the backing storage so the initial fill avoids at least some resize work; non-collection enumerables fall back to the caller-supplied capacity parameter. The out-of-band zero-key / default-key slot is populated correctly when the source contains an entry with default(TKey). Completes the last of the milestone 1.1.0 API-parity items on the dictionaries.
  • IEnumerableConstructorTests — 28 tests covering null-source and invalid-load-factor validation, empty sources, array / list / non-collection enumerable sources, duplicate-key detection (including duplicate zero / default keys), zero-key and null-reference-key capture, 500-entry large-source round-trip, source-independence after construction, caller-specified capacity dominating the source count, cross-dictionary copy via Select, and projection through IReadOnlyDictionary<,> to verify the new ctor flows into the existing interface surface.
  • IReadOnlyDictionary<TKey, TValue?> implementation on both CelerityDictionary<TKey, TValue, THasher> and IntDictionary<TValue, THasher> — the dictionaries can now be passed to any API that accepts IReadOnlyDictionary<,> (LINQ, DI, BCL ToDictionary, etc.) without a wrapper. The implementation is a thin set of explicit interface forwarders on top of the existing struct KeyCollection / ValueCollection views and struct Enumerator, so the zero-allocation foreach (var kvp in map) / foreach (var k in map.Keys) fast paths remain unchanged and the interface path boxes the enumerator exactly once per call, matching BCL Dictionary<,> behaviour. The out-of-band default-key / zero-key entry is surfaced through every interface member (ContainsKey, TryGetValue, indexer, Keys, Values, generic IEnumerable<KeyValuePair<TKey, TValue?>>, and non-generic IEnumerable), and mid-enumeration mutation still throws InvalidOperationException. Closes issue #9; completes the last of the 1.1.0 API-parity collection work.
  • ReadOnlyDictionaryInterfaceTests — boxed-path coverage for both dictionaries through the IReadOnlyDictionary<TKey, TValue?> surface: indexer, ContainsKey, TryGetValue, Keys / Values widened to IEnumerable<T>, generic IEnumerable<KeyValuePair<,>> and non-generic IEnumerable enumeration, default-key / zero-key / null-reference-key inclusion, mutation-during-enumeration detection on the boxed enumerator, Enumerable.Count() LINQ dispatch, and a polymorphic consumer function proving both dictionary shapes flow through the same IReadOnlyDictionary<int, int> parameter.
  • CelerityDictionary.GetEnumerator(), CelerityDictionary.Keys, and CelerityDictionary.Values — struct-based, allocation-free enumeration over a CelerityDictionary<TKey, TValue, THasher>, mirroring the IntDictionary surface added earlier in 1.1.0. Keys and Values expose KeyCollection / ValueCollection readonly structs, each with their own struct enumerator, so foreach (var kvp in map) / foreach (var k in map.Keys) / foreach (var v in map.Values) do not box. The out-of-band default-key entry is yielded first — including null for reference-type keys. The enumerators track a _version counter and throw InvalidOperationException on MoveNext / Reset if the dictionary is mutated mid-enumeration, matching BCL Dictionary<,> semantics. Completes issue #10 and unblocks IReadOnlyDictionary<TKey, TValue> (#9).
  • CelerityDictionaryEnumerationTests — mirror of IntDictionaryEnumerationTests covering empty / single / many-entry enumeration, default-key-first ordering for both value-type and reference-type (null) keys, Remove / Clear / resize survival, mutation-during-enumeration detection on insert / overwrite / default-key-insert / remove / clear, Reset reuse, Keys.Count / Values.Count tracking, and IEnumerable<T> interface parity.
  • IntDictionary.GetEnumerator(), IntDictionary.Keys, and IntDictionary.Values — struct-based, allocation-free enumeration over an IntDictionary<TValue, THasher>. Keys and Values expose KeyCollection / ValueCollection readonly structs, each with their own struct enumerator, so foreach (var kvp in map) / foreach (int k in map.Keys) / foreach (var v in map.Values) do not box. The out-of-band zero-key entry is yielded first. The enumerators track a _version counter and throw InvalidOperationException on MoveNext / Reset if the dictionary is mutated mid-enumeration, matching BCL Dictionary<,> semantics. First step toward implementing IReadOnlyDictionary<int, TValue> (#10).
  • Int32Murmur3Hasher in Celerity.Hashing — Murmur3 32-bit finalizer ("fmix32") for int keys. Struct hasher, AggressiveInlining. Provides excellent avalanche properties; prefer over Int32WangNaiveHasher when key distribution is clustered or adversarial. Maps 0 → 0 (fixed point of fmix32).
  • Int64WangHasher in Celerity.Hashing — Thomas Wang 64-bit integer hash for long keys. Struct hasher, AggressiveInlining. Faster than Int64Murmur3Hasher while providing better avalanche than a simple XOR-fold; prefer when throughput matters more than adversarial collision resistance. Invertible (bijective on ulong) so truncation to 32 bits is the only source of collisions.
  • Int32Murmur3HasherTests — exact anchor values for key extremes, determinism, high-bit avalanche check, 1000-value distinctness sweep, and integration tests driving CelerityDictionary and CeleritySet including the default(int) out-of-band slot.
  • Int64WangHasherTests — exact anchor values for key extremes, determinism, high-bit avalanche check, 1000-value distinctness sweep, and integration tests driving CelerityDictionary and CeleritySet including the default(long) out-of-band slot.
  • GuidHasher in Celerity.Hashing — reinterprets the 128-bit Guid as two 64-bit halves, runs Murmur3 fmix64 on each, and XORs the mixed halves. Struct hasher, AggressiveInlining, zero-allocation (no stack buffer — reinterpret via Unsafe.As<Guid, ulong>). Prefer over DefaultHasher<Guid> on hot paths: fully inlineable and avoids the EqualityComparer<T>.Default virtual dispatch.
  • GuidHasherTestsGuid.Empty → 0 anchor, determinism across calls and struct instances, avalanche on both the low and high 64-bit halves, shared-prefix/shared-suffix divergence (guards against hashers that weight one half too heavily), two 1000-value distinctness sweeps (sequential low-half keys and Guid.NewGuid()), and integration tests confirming GuidHasher satisfies the hasher constraint on CeleritySet<Guid,THasher> and CelerityDictionary<Guid,TValue,THasher> (including the Guid.Empty out-of-band slot).
  • UInt32Hasher in Celerity.Hashing — Wang/Jenkins-style bit-mixer for uint keys. Struct hasher, AggressiveInlining. Counterpart to Int32WangNaiveHasher.
  • UInt64Hasher in Celerity.Hashing — Murmur3 fmix64 finalizer for ulong keys. Struct hasher, AggressiveInlining. Counterpart to Int64Murmur3Hasher.
  • UInt32HasherTests and UInt64HasherTests — exact-value cases (including values crossing the sign bit), determinism, avalanche on the top bit, and a 1000-value distinctness sweep for the 64-bit mixer.
  • DefaultHasher<T> in Celerity.Hashing — a general-purpose IHashProvider<T> that delegates to EqualityComparer<T>.Default.GetHashCode(). Use it when no specialized hasher exists for a type (e.g. Guid, custom structs, or reference types). It is a struct, so the JIT devirtualizes the outer call on the probe path; the inner EqualityComparer<T> dispatch is unavoidable but acceptable for non-hot-path types.
  • XML doc comments added to IHashProvider<T>, Int32WangNaiveHasher, Int64Murmur3Hasher, and StringFnV1AHasher. All public hasher types now carry full XML documentation.
  • DefaultHasherTests — verifies BCL contract equivalence for int, string, and Guid keys; determinism across calls and struct instances; and integration tests confirming DefaultHasher<T> satisfies the hasher constraints on CeleritySet<T,THasher>, IntSet<THasher>, and CelerityDictionary<TKey,TValue,THasher>.
  • Add(TKey, TValue) on CelerityDictionary and IntDictionary — inserts a key/value pair and throws ArgumentException if the key already exists, matching BCL Dictionary<,> semantics.
  • TryAdd(TKey, TValue) on CelerityDictionary and IntDictionary — inserts without overwriting; returns true on success, false if the key already exists. Both methods correctly handle the zero/default-key out-of-band slot.
  • TryGetValue(TKey, out TValue?) on CelerityDictionary and IntDictionary, following BCL semantics.
  • Clear() on CelerityDictionary and IntDictionary — resets the map without releasing the backing arrays, so pooled/reused instances don't pay an allocation on every generation.
  • .github/workflows/ci.ymldotnet build and dotnet test now run automatically on every push to main and every pull request.
  • ROADMAP.md — prioritized plan through 1.0.
  • ISSUES.md — snapshot of the known issue backlog.
  • CONTRIBUTING.md — build, test, and PR conventions.
  • CHANGELOG.md — this file.
  • Forced-collision test suites for both IntDictionary and CelerityDictionary using a constant-hash IHashProvider, exercising insert, overwrite, remove, remove-then-reinsert, and resize under maximum probing pressure.
  • String-key tests for CelerityDictionary covering the null default-key path (null insert, remove, TryGetValue, Clear).
  • Remove-then-reinsert stress test for CelerityDictionary with the standard hasher (parity with the existing IntDictionary test).
  • Load-factor boundary test suite (LoadFactorBoundaryTests.cs) covering low load factor (0.5), high load factor (0.95), multiple sequential resizes from a tiny initial capacity, default/zero-key coexistence with the resize threshold, and a parameterized Theory across {0.25, 0.5, 0.75, 0.95} for both IntDictionary and CelerityDictionary. Closes the remaining gap from issue #7.

Fixed

  • IntDictionary<TValue> constructor arguments were silently discarded. The convenience subclass IntDictionary<TValue> accepted capacity and loadFactor parameters but forwarded to : base() with no arguments, so every instance was created with the defaults regardless of what the caller passed. It now forwards capacity and loadFactor to the base constructor.

  • IntDictionary could not store the key 0. EMPTY_KEY = 0 was used as the "empty slot" sentinel, which collided with the legitimate key value 0. map[0] = x appeared to succeed but subsequent ContainsKey(0), map[0], and Count returned wrong answers. The zero key is now stored out-of-band via a dedicated flag + value slot, a pattern borrowed from fastutil / HPPC.

  • CelerityDictionary could not store default(TKey). Same root cause as above, generalized: default(int) / default(long) / default(Guid) / null strings were all lost. Fixed the same way, via a _hasDefaultKey flag and a dedicated value slot.

  • Constructor validation test suite (ConstructorValidationTests.cs) covering rejection of invalid loadFactor (≤0, ≥1) and negative capacity for both IntDictionary and CelerityDictionary, plus acceptance of valid edge values.

Fixed (additional)

  • CelerityDictionary and IntDictionary accepted invalid constructor arguments. loadFactor >= 1.0 caused an infinite loop in ProbeForInsert once the table was full; loadFactor <= 0 caused a resize on every insert. Both constructors now throw ArgumentOutOfRangeException for capacity < 0, loadFactor <= 0, or loadFactor >= 1.

Changed

  • TryAdd (and therefore Add) on IntDictionary<TValue, THasher>, CelerityDictionary<TKey, TValue, THasher>, IntSet<THasher>, and CeleritySet<T, THasher> now walks the probe chain exactly once per call instead of twice. The previous implementation called ContainsKey / Contains followed by the indexer setter / InsertNon* helper, each starting its own probe walk; the rewrite uses a single ProbeForInsert-style walk that either lands on the existing entry (return false) or on the first empty slot (insert in place). Behaviour is identical to before — including the duplicate-key contract on Add and the "unchanged on duplicate" contract on TryAdd — but bulk-loads via the new IEnumerable<KeyValuePair<,>> constructor and any Add-heavy hot path now do roughly half the probe work. Closes issue #24. Pinned by TryAddProbeCountTests, which uses a counting IHashProvider to assert that TryAdd calls Hash exactly once on both the new-key and duplicate-key paths across all four collections.
  • The IntDictionary EMPTY_VALUE field is now static readonly instead of an instance field. No behavior change; just removes per-instance overhead.