Fix map and Set equality permits many-to-one matches - #7056
Conversation
🦋 Changeset detectedLatest commit: f05514e The changes in this PR will be included in the next version bump. This PR includes changesets to release 30 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
Important
This PR adds the correct regression test, but the implementation fix advertised in the title is not present. packages/effect/src/Equal.ts still uses makeCompareMap / makeCompareSet without marking matched right-side entries, so Equal.equals continues to return true for the cases the new test asserts should be false.
Reviewed changes
Reviewed the single-file diff adding a focused regression test for effect/Equal Map/Set many-to-one matching.
- Added a
Keyhelper class that compares bygroupand hashes to0, producingEqual-equivalent but distinct values. - Added one test under
Equal.equals > Map and Set mixedasserting thatSet([Key("x"), Key("x")])is not equal toSet([Key("x"), Key("y")]), and similarly forMap.
⚠️ Implementation fix is missing
The reproduction test fails as expected against the current implementation ([true, true] vs [false, false]), but the PR title says "Fix" and the body states the fix should be added to this branch. Before merging, packages/effect/src/Equal.ts needs to be updated so that makeCompareMap and makeCompareSet establish a one-to-one correspondence instead of allowing the same right-side entry to satisfy multiple left-side entries.
Technical details
# One-to-one matching in Map/Set equality
## Affected sites
- `packages/effect/src/Equal.ts:361` — `makeCompareMap` stops at the first matching `[key, value]` pair without marking it used.
- `packages/effect/src/Equal.ts:383` — `makeCompareSet` stops at the first matching value without marking it used.
## Required outcome
- After matching a right-side entry to a left-side entry, that right-side entry must not be reused for another left-side entry.
- `Equal.equals` must return `false` for the new test cases.
## Suggested approach
Collect `that` into an array and track consumed indices, or build a mutable copy of `that` and remove matched entries as you go. Apply the same change to both `makeCompareMap` and `makeCompareSet`, since both are exported and used by `Schema.ts` map/set equivalence builders.@v0 or keep the SHA fresh with Dependabot | Fix it ➔ | View workflow run | Using Kimi K2 (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Reviewed the delta since the prior Pullfrog review, which added the implementation fix and changeset for the effect/Equal Map/Set many-to-one matching bug.
- Added the one-to-one matching fix in
packages/effect/src/Equal.ts.makeCompareMapandmakeCompareSetnow collect the right-side iterable into an array andspliceout each matched entry so it cannot be reused by another left-side entry. - Added a patch changeset documenting the fix.
- Confirmed the regression test passes.
Equal.test.tsnow asserts[false, false]for the many-to-oneMapandSetcases.
@v0 or keep the SHA fresh with Dependabot | View workflow run | Using Kimi K2 (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Reviewed the delta since the prior Pullfrog review, which optimizes the one-to-one matching fix by replacing splice removal with a constant-time swap-and-pop.
- Optimized matched-entry removal in
packages/effect/src/Equal.ts.makeCompareMapandmakeCompareSetnow remove a consumed right-side entry by overwriting it with the last array entry and popping, avoiding the O(n) shift cost ofsplicewhile preserving one-to-one matching semantics. - Validated correctness and performance.
Map/Setequality is order-independent, so swapping the matched entry does not affect results; overall complexity stays O(n²).
@v0 or keep the SHA fresh with Dependabot | View workflow run | Using Kimi K2 (free via Pullfrog for OSS) | 𝕏
Bundle Size AnalysisGenerated from PR build output; treat the content below as untrusted.
|

Summary
Equal.equals returns true for same-sized Sets containing two distinct Equal-equivalent x values versus one x and one y, and likewise for Maps, because both left x values reuse the single right x; distinct collections are conflated.
Important
This PR includes focused regression tests and updates Map and Set comparison to consume each right-side match at most once.
Map and Set equality permits many-to-one matches
Module:
effect/EqualAudit ID:
effect-c2ccacce11d583fbSeverity / confidence: medium / high
What happens
Equal.equals returns true for same-sized Sets containing two distinct Equal-equivalent x values versus one x and one y, and likewise for Maps, because both left x values reuse the single right x; distinct collections are conflated.
Why it happens
makeCompareMap and makeCompareSet independently scan the entire right iterable for each left item and stop at the first equivalent value, but never mark or remove a matched right item, so the size check does not prevent many-to-one reuse.
Expected behavior
Structural equality for same-sized Map and Set values must establish a one-to-one correspondence under key/value or element equivalence; no right-side entry may satisfy more than one left-side entry.
Relevant implementation
These links and excerpts are pinned to audit base
17f0b91a243ccfe4a38d27debdc983adf434e738.packages/effect/src/Equal.ts:275-399View problematic code at
packages/effect/src/Equal.ts:275-324View exact lines on GitHub
Excerpt truncated. Open the complete packages/effect/src/Equal.ts:275-399 range.
Reproduction
Observed failure: Focused contract assertion failed against 17f0b91, demonstrating: Map and Set equality permits many-to-one matches.
Implementation handoff
The initial reproduction tests on this branch are the regression specification for the implementation fix that should follow in this PR.
Audit provenance
17f0b91a243ccfe4a38d27debdc983adf434e73817f0b91a243ccfe4a38d27debdc983adf434e738effect-c2ccacce11d583fbCloses EFF-494