v1.1.2
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.xmldocumentation 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.csprojonly; the test and benchmark projects are unaffected because they do not set<GenerateDocumentationFile>true</GenerateDocumentationFile>.
Added
- Cross-platform CI matrix —
.github/workflows/ci.ymlnow runsdotnet buildanddotnet testonubuntu-latest,windows-latest, andmacos-latestfor every push tomainand every pull request, withfail-fast: falseso 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 bothCelerityDictionary<TKey, TValue, THasher>andIntDictionary<TValue, THasher>. The captured value is the value that was associated with the key immediately before removal, and isdefault(TValue)when the key was not found. The out-of-band default-key / zero-key slot is surfaced through the same path. The existing voidRemove(key)overload now delegates to this method, so the rehash-after-remove path is unchanged and continues to bump the dictionary's_versioncounter so active enumerators throwInvalidOperationExceptionon the nextMoveNext. Closes issue #23.RemoveOutValueTests— coverage for both dictionaries: standard-key capture, missing-key returningfalseanddefault, 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 voidRemoveoverload still returnstrueafter the delegation refactor.LongDictionary<TValue, THasher>and theLongDictionary<TValue>convenience subclass — high-performance dictionary keyed bylong, mirroring theIntDictionarysurface for 64-bit keys. Defaults toInt64WangHasher. ImplementsIReadOnlyDictionary<long, TValue?>, ships allocation-free struct enumerator andKeyCollection/ValueCollectionviews, theIEnumerable<KeyValuePair<long, TValue>>constructor, fullAdd/TryAdd/TryGetValue/Clearsurface, and constructor validation matchingIntDictionary. The zero key (0L) collides with theEMPTY_KEYsentinel exactly as onIntDictionaryand is stored out-of-band via a dedicated flag + value slot, somap[0L] = xis round-trippable. Closes the last collection-shape gap in milestone 1.1.0.LongDictionaryTests— mirrors theIntDictionaryTestscoverage (CRUD, zero-key, resize survival, remove-then-reinsert, constructor validation,IEnumerableconstructor,Add/TryAddduplicate semantics, struct-enumerator andKeys/Valuesviews, and the boxedIReadOnlyDictionary<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()andIEnumerable<T>conformance — struct-based, allocation-free enumeration over aCeleritySet<T, THasher>. The out-of-banddefault(T)entry (zero for primitives,Guid.EmptyforGuid,nullfor reference-type elements) is yielded first; the rest of the elements follow in unspecified order. The struct enumerator tracks a_versioncounter (bumped on every entry-point structural mutation:Add,TryAdd,Remove,Clear) and throwsInvalidOperationExceptiononMoveNext/Resetif the set is mutated mid-enumeration, matching BCLHashSet<T>semantics. Closes the second slice of issue #23 (IntSetshipped in PR #50); the sets are now at full enumeration parity with the dictionaries. Unblocks the post-1.1.0IReadOnlySet<T>interface and anIEnumerable<T>constructor mirroring dictionary issue #22.CeleritySetEnumerationTests— 26 tests mirroringIntSetEnumerationTests, covering empty / single / many-entry enumeration, default-value-first ordering for both value-type and reference-type (null) elements, removal / default-removal /Clearsurvival, multi-resize survival from a tiny initial capacity, mutation-during-enumeration detection on insert / default-insert / remove / default-remove / clear,Resetinvalidation under mutation, no-op mutations (re-adding an existing item, re-adding a present default item, removing an absent item,Clearon an empty set) preserving enumerator validity,Resetreusability, post-exhaustionCurrentreset, generic and non-genericIEnumerableparity,CeleritySet<int, Int32Murmur3Hasher>open-generic coverage, and a LINQ smoke test (Count/Sum/Contains) confirming the boxed enumerator path.IntSet.GetEnumerator()andIEnumerable<int>conformance — struct-based, allocation-free enumeration over anIntSet<THasher>. The out-of-band zero entry is yielded first; the rest of the elements follow in unspecified order. The struct enumerator tracks a_versioncounter (bumped on every entry-point structural mutation:Add,TryAdd,Remove,Clear) and throwsInvalidOperationExceptiononMoveNext/Resetif the set is mutated mid-enumeration, matching BCLHashSet<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 (CeleritySetfollows in a separate PR). Unblocks theIEnumerable<int>constructor and the post-1.1.0IReadOnlySet<int>interface.IntSetEnumerationTests— 25 tests covering empty / single / many-entry enumeration, zero-first-when-present ordering, removal / zero-removal /Clearsurvival, multi-resize survival from a tiny initial capacity, mutation-during-enumeration detection on insert / zero-insert / remove / zero-remove / clear,Resetinvalidation under mutation, no-op mutations (re-adding an existing item, removing an absent item,Clearon an empty set) preserving enumerator validity,Resetreusability, post-exhaustionCurrentreset, generic and non-genericIEnumerableparity,IntSet<Int32Murmur3Hasher>open-generic coverage, and a LINQ smoke test (Count/Sum/Contains) confirming the boxed enumerator path.IEnumerable<KeyValuePair<TKey, TValue>>constructor on bothCelerityDictionary<TKey, TValue, THasher>andIntDictionary<TValue, THasher>(and theIntDictionary<TValue>convenience subclass). Matches BCLDictionary<,>semantics: throwsArgumentNullExceptionon a null source andArgumentExceptionon duplicate keys (including duplicate zero / default keys). When the source implementsICollection<T>, itsCountis used to size the backing storage so the initial fill avoids at least some resize work; non-collection enumerables fall back to the caller-suppliedcapacityparameter. The out-of-band zero-key / default-key slot is populated correctly when the source contains an entry withdefault(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 viaSelect, and projection throughIReadOnlyDictionary<,>to verify the new ctor flows into the existing interface surface.IReadOnlyDictionary<TKey, TValue?>implementation on bothCelerityDictionary<TKey, TValue, THasher>andIntDictionary<TValue, THasher>— the dictionaries can now be passed to any API that acceptsIReadOnlyDictionary<,>(LINQ, DI, BCLToDictionary, etc.) without a wrapper. The implementation is a thin set of explicit interface forwarders on top of the existing structKeyCollection/ValueCollectionviews and structEnumerator, so the zero-allocationforeach (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 BCLDictionary<,>behaviour. The out-of-band default-key / zero-key entry is surfaced through every interface member (ContainsKey,TryGetValue, indexer,Keys,Values, genericIEnumerable<KeyValuePair<TKey, TValue?>>, and non-genericIEnumerable), and mid-enumeration mutation still throwsInvalidOperationException. Closes issue #9; completes the last of the 1.1.0 API-parity collection work.ReadOnlyDictionaryInterfaceTests— boxed-path coverage for both dictionaries through theIReadOnlyDictionary<TKey, TValue?>surface: indexer,ContainsKey,TryGetValue,Keys/Valueswidened toIEnumerable<T>, genericIEnumerable<KeyValuePair<,>>and non-genericIEnumerableenumeration, 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 sameIReadOnlyDictionary<int, int>parameter.CelerityDictionary.GetEnumerator(),CelerityDictionary.Keys, andCelerityDictionary.Values— struct-based, allocation-free enumeration over aCelerityDictionary<TKey, TValue, THasher>, mirroring theIntDictionarysurface added earlier in 1.1.0.KeysandValuesexposeKeyCollection/ValueCollectionreadonly structs, each with their own struct enumerator, soforeach (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 — includingnullfor reference-type keys. The enumerators track a_versioncounter and throwInvalidOperationExceptiononMoveNext/Resetif the dictionary is mutated mid-enumeration, matching BCLDictionary<,>semantics. Completes issue #10 and unblocksIReadOnlyDictionary<TKey, TValue>(#9).CelerityDictionaryEnumerationTests— mirror ofIntDictionaryEnumerationTestscovering 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,Resetreuse,Keys.Count/Values.Counttracking, andIEnumerable<T>interface parity.IntDictionary.GetEnumerator(),IntDictionary.Keys, andIntDictionary.Values— struct-based, allocation-free enumeration over anIntDictionary<TValue, THasher>.KeysandValuesexposeKeyCollection/ValueCollectionreadonly structs, each with their own struct enumerator, soforeach (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_versioncounter and throwInvalidOperationExceptiononMoveNext/Resetif the dictionary is mutated mid-enumeration, matching BCLDictionary<,>semantics. First step toward implementingIReadOnlyDictionary<int, TValue>(#10).Int32Murmur3HasherinCelerity.Hashing— Murmur3 32-bit finalizer ("fmix32") forintkeys. Struct hasher,AggressiveInlining. Provides excellent avalanche properties; prefer overInt32WangNaiveHasherwhen key distribution is clustered or adversarial. Maps0 → 0(fixed point of fmix32).Int64WangHasherinCelerity.Hashing— Thomas Wang 64-bit integer hash forlongkeys. Struct hasher,AggressiveInlining. Faster thanInt64Murmur3Hasherwhile providing better avalanche than a simple XOR-fold; prefer when throughput matters more than adversarial collision resistance. Invertible (bijective onulong) 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 drivingCelerityDictionaryandCeleritySetincluding thedefault(int)out-of-band slot.Int64WangHasherTests— exact anchor values for key extremes, determinism, high-bit avalanche check, 1000-value distinctness sweep, and integration tests drivingCelerityDictionaryandCeleritySetincluding thedefault(long)out-of-band slot.GuidHasherinCelerity.Hashing— reinterprets the 128-bitGuidas two 64-bit halves, runs Murmur3fmix64on each, and XORs the mixed halves. Struct hasher,AggressiveInlining, zero-allocation (no stack buffer — reinterpret viaUnsafe.As<Guid, ulong>). Prefer overDefaultHasher<Guid>on hot paths: fully inlineable and avoids theEqualityComparer<T>.Defaultvirtual dispatch.GuidHasherTests—Guid.Empty → 0anchor, 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 andGuid.NewGuid()), and integration tests confirmingGuidHashersatisfies the hasher constraint onCeleritySet<Guid,THasher>andCelerityDictionary<Guid,TValue,THasher>(including theGuid.Emptyout-of-band slot).UInt32HasherinCelerity.Hashing— Wang/Jenkins-style bit-mixer foruintkeys. Struct hasher,AggressiveInlining. Counterpart toInt32WangNaiveHasher.UInt64HasherinCelerity.Hashing— Murmur3fmix64finalizer forulongkeys. Struct hasher,AggressiveInlining. Counterpart toInt64Murmur3Hasher.UInt32HasherTestsandUInt64HasherTests— 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>inCelerity.Hashing— a general-purposeIHashProvider<T>that delegates toEqualityComparer<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 innerEqualityComparer<T>dispatch is unavoidable but acceptable for non-hot-path types.- XML doc comments added to
IHashProvider<T>,Int32WangNaiveHasher,Int64Murmur3Hasher, andStringFnV1AHasher. 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 confirmingDefaultHasher<T>satisfies the hasher constraints onCeleritySet<T,THasher>,IntSet<THasher>, andCelerityDictionary<TKey,TValue,THasher>.Add(TKey, TValue)onCelerityDictionaryandIntDictionary— inserts a key/value pair and throwsArgumentExceptionif the key already exists, matching BCLDictionary<,>semantics.TryAdd(TKey, TValue)onCelerityDictionaryandIntDictionary— inserts without overwriting; returnstrueon success,falseif the key already exists. Both methods correctly handle the zero/default-key out-of-band slot.TryGetValue(TKey, out TValue?)onCelerityDictionaryandIntDictionary, following BCL semantics.Clear()onCelerityDictionaryandIntDictionary— resets the map without releasing the backing arrays, so pooled/reused instances don't pay an allocation on every generation..github/workflows/ci.yml—dotnet buildanddotnet testnow run automatically on every push tomainand 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
IntDictionaryandCelerityDictionaryusing a constant-hashIHashProvider, exercising insert, overwrite, remove, remove-then-reinsert, and resize under maximum probing pressure. - String-key tests for
CelerityDictionarycovering thenulldefault-key path (nullinsert, remove,TryGetValue,Clear). - Remove-then-reinsert stress test for
CelerityDictionarywith the standard hasher (parity with the existingIntDictionarytest). - 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 bothIntDictionaryandCelerityDictionary. Closes the remaining gap from issue #7.
Fixed
-
IntDictionary<TValue>constructor arguments were silently discarded. The convenience subclassIntDictionary<TValue>acceptedcapacityandloadFactorparameters but forwarded to: base()with no arguments, so every instance was created with the defaults regardless of what the caller passed. It now forwardscapacityandloadFactorto the base constructor. -
IntDictionarycould not store the key0.EMPTY_KEY = 0was used as the "empty slot" sentinel, which collided with the legitimate key value0.map[0] = xappeared to succeed but subsequentContainsKey(0),map[0], andCountreturned wrong answers. The zero key is now stored out-of-band via a dedicated flag + value slot, a pattern borrowed fromfastutil/HPPC. -
CelerityDictionarycould not storedefault(TKey). Same root cause as above, generalized:default(int)/default(long)/default(Guid)/nullstrings were all lost. Fixed the same way, via a_hasDefaultKeyflag and a dedicated value slot. -
Constructor validation test suite (
ConstructorValidationTests.cs) covering rejection of invalidloadFactor(≤0, ≥1) and negativecapacityfor bothIntDictionaryandCelerityDictionary, plus acceptance of valid edge values.
Fixed (additional)
CelerityDictionaryandIntDictionaryaccepted invalid constructor arguments.loadFactor >= 1.0caused an infinite loop inProbeForInsertonce the table was full;loadFactor <= 0caused a resize on every insert. Both constructors now throwArgumentOutOfRangeExceptionforcapacity < 0,loadFactor <= 0, orloadFactor >= 1.
Changed
TryAdd(and thereforeAdd) onIntDictionary<TValue, THasher>,CelerityDictionary<TKey, TValue, THasher>,IntSet<THasher>, andCeleritySet<T, THasher>now walks the probe chain exactly once per call instead of twice. The previous implementation calledContainsKey/Containsfollowed by the indexer setter /InsertNon*helper, each starting its own probe walk; the rewrite uses a singleProbeForInsert-style walk that either lands on the existing entry (returnfalse) or on the first empty slot (insert in place). Behaviour is identical to before — including the duplicate-key contract onAddand the "unchanged on duplicate" contract onTryAdd— but bulk-loads via the newIEnumerable<KeyValuePair<,>>constructor and anyAdd-heavy hot path now do roughly half the probe work. Closes issue #24. Pinned byTryAddProbeCountTests, which uses a countingIHashProviderto assert thatTryAddcallsHashexactly once on both the new-key and duplicate-key paths across all four collections.- The
IntDictionaryEMPTY_VALUEfield is nowstatic readonlyinstead of an instance field. No behavior change; just removes per-instance overhead.