Skip to content

[Repo Assist] fix: PersistentHashMap.containsKey returns true for None/unit/null values#238

Open
github-actions[bot] wants to merge 4 commits intomasterfrom
repo-assist/fix-issue-85-persistenthashmap-containskey-null-515c57e49f6b6820
Open

[Repo Assist] fix: PersistentHashMap.containsKey returns true for None/unit/null values#238
github-actions[bot] wants to merge 4 commits intomasterfrom
repo-assist/fix-issue-85-persistenthashmap-containskey-null-515c57e49f6b6820

Conversation

@github-actions
Copy link
Contributor

@github-actions github-actions bot commented Mar 8, 2026

🤖 This PR was created by Repo Assist, an automated AI assistant.

Fixes the long-standing bug reported in #85: PersistentHashMap.containsKey (and the TransientHashMap counterpart) returned false for keys whose associated value is null at the .NET level — including None, unit, and explicit null.

Root Cause

ContainsKey called INode.find, which returns the stored value as obj. The result was compared to null to detect presence:

else this.root.find(0, hash(key), key) <> null

When the stored value is null (as it is for None, unit, or any null-valued reference type), this comparison yields false even though the key is present.

Fix

INode already had a tryFind method on all node types, returning obj option. Some null is correctly distinguished from None, so we change ContainsKey to use it:

else this.root.tryFind(0, hash(key), key).IsSome

Applied to both PersistentHashMap<'T,'S>.ContainsKey and TransientHashMap<'T,'S>.ContainsKey.

Tests

  • Activated the two previously-ptest (pending) tests for None values in both PersistentHashMapTest.fs and TransientHashMapTest.fs.
  • Added three new tests to PersistentHashMapTest.fs:
    • containsKey with unit value
    • containsKey after multiple adds mixing None and Some
    • Original "None value in empty map" test (activated)

Trade-offs

tryFind allocates an option discriminated union on every ContainsKey call. In the common case (non-null values), this is a minor allocation cost. The alternative — adding a dedicated containsKey method to INode returning bool — would be more performant but requires more changes; this minimal fix is correct and low-risk.

Test Status

✅ All 710 tests pass (dotnet testPassed: 710, Skipped: 4, Failed: 0). The skipped tests are pre-existing skips unrelated to this change.

Closes #85

Generated by Repo Assist ·

To install this agentic workflow, run

gh aw add githubnext/agentics/workflows/repo-assist.md@30f2254f2a7a944da1224df45d181a3f8faefd0d

…t values

ContainsKey was comparing the result of INode.find (which returns the
value as obj) against null. This caused false negatives when the stored
value is null at the .NET level — which happens for None, unit, and
explicitly null values.

Fix: use INode.tryFind (which already existed on all node types) in
ContainsKey for both PersistentHashMap and TransientHashMap. tryFind
returns an option, so Some null (i.e. key found, value is null) is
correctly distinguished from None (key not found).

Activate the two previously-pending tests and add three new tests
covering None, unit, and mixed Some/None scenarios.

Closes #85

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dsyme
Copy link
Contributor

dsyme commented Mar 8, 2026

/repo-assist format the code according to AGENTS.md

@dsyme
Copy link
Contributor

dsyme commented Mar 8, 2026

/repo-assist This is not the right tradeoff:

tryFind allocates an option discriminated union on every ContainsKey call. In the common case (non-null values), this is a minor allocation cost. The alternative — adding a dedicated containsKey method to INode returning bool — would be more performant but requires more changes; this minimal fix is correct and low-risk.

Instead you should indeed have a dedicated containsKey returning bool. Performance matters for collections!

github-actions bot and others added 2 commits March 8, 2026 21:39
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… allocation

Replace the tryFind-based ContainsKey with a new containsKey method on INode
that returns bool directly. This avoids allocating an 'option' discriminated
union on every ContainsKey call, improving performance for this hot path.

Implement containsKey on all three INode types:
- HashCollisionNode: check findIndex >= 0 and key equality
- ArrayNode: delegate to child node.containsKey
- BitmapIndexedNode: check bitmap bit, then key equality or recurse into sub-node

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Contributor Author

github-actions bot commented Mar 8, 2026

Commit pushed: 4cd402e

Generated by Repo Assist

@github-actions

This comment has been minimized.

@github-actions
Copy link
Contributor Author

github-actions bot commented Mar 8, 2026

Commit pushed: 4cd402e

Generated by Repo Assist

@github-actions
Copy link
Contributor Author

github-actions bot commented Mar 8, 2026

🤖 This is an automated response from Repo Assist.

Done! I've also addressed your performance feedback. I've added a dedicated containsKey: int * int * obj -> bool method to the INode interface and implemented it on all three node types (HashCollisionNode, ArrayNode, BitmapIndexedNode). Both ContainsKey methods now call this.root.containsKey(...) directly — no option allocation.

All 710 tests pass (same as before, 4 pre-existing skips).

Generated by Repo Assist for issue #238 ·

To install this agentic workflow, run

gh aw add githubnext/agentics/workflows/repo-assist.md@30f2254f2a7a944da1224df45d181a3f8faefd0d

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PersistentHashMap.containsKey is broken for null values

1 participant