Skip to content

Deprecate MapOps.KeySet, replace with private (and more performant) strict/lazy KeySet implementations#24767

Merged
WojciechMazur merged 7 commits intoscala:mainfrom
natsukagami:i-24749
Jan 7, 2026
Merged

Deprecate MapOps.KeySet, replace with private (and more performant) strict/lazy KeySet implementations#24767
WojciechMazur merged 7 commits intoscala:mainfrom
natsukagami:i-24749

Conversation

@natsukagami
Copy link
Copy Markdown
Contributor

@natsukagami natsukagami commented Dec 16, 2025

Fixes #24749.

The PR contains two parts, one tiny and the other more involved.

KeySet performance issues

Previously we use a List[K] to hold keys inside the strict KeySet, in order to
keep iteration order stable. However this degrades performance of KeySet as a Set.
The fix is to simply use something that has both. Currently it means mutable.LinkedHashSet,
so we use it, even though we don't intend to mutate the set at all.

Deprecating KeySet as a whole and use our private implementations instead

As part of #23769 we categorised most data structures to be strict/pure (as in "not hiding any
computation as part of its structure"), which includes Set. It turns out that our change to MapOps.GenKeySet in #23769 was never backwards-compatible -- it creates abstract getters and setters for the allKeys val.

This however clashes with how .keySet method was implemented: it returns a Set[K], but
provides no guarantee on whether it is a lazy view over the underlying data structure
(which can be lazy, e.g. when you call .keySet from a MapView) or a strict set.
With API compatibility in mind, we now create strict key sets (with a copy of the keys)
in the generic case, while trying to detect whether the .keySet method was called from an
already strict data-structure (which is the majority case) - returning a thin-wrapper LazyKeySet
in that case. As the LazyKeySet works like a view with no aggregation, applying it over a strict
data structure doesn't violate strictness.

This left a few holes however, which was overlooked: some data structures such as mutable.HashSet
overrides .keySet with their own (protected!) classes, extending from the previously-ambiguous
KeySet classes (that is now made strict), and accidentally ending up always using the strict version.

Unfortunately we cannot modify these extended classes (for TASTy compatibility), so similar versions
extending the thin wrapper are added instead, and the old classes deprecated.
To not shoot ourselves in the foot in the future, these classes are also made private[collection]:
We don't expect classes like HashMap to be extended with a custom KeySet, and in the cases that
it does, it is not a problem to reimplement the missing 4-5 methods in the most efficient way for your
extended data structure.

@natsukagami natsukagami requested a review from a team as a code owner December 16, 2025 17:59
@Gedochao Gedochao added the backport:nominated If we agree to backport this PR, replace this tag with "backport:accepted", otherwise delete it. label Dec 17, 2025
@Gedochao Gedochao added this to the 3.8.0 milestone Dec 17, 2025
@natsukagami natsukagami marked this pull request as draft December 17, 2025 17:37
@natsukagami
Copy link
Copy Markdown
Contributor Author

It turns out that our change to MapOps.GenKeySet in #23769 was never backwards-compatible -- it creates abstract getters and setters for the allKeys val.

I will make a commit that rolls back this change, add some unsafeDiscardUses (which requires #24273 to be backported to the next RC) to make GenKeySet capture-check, and then completely deprecate this trait.

@WojciechMazur
Copy link
Copy Markdown
Contributor

Reference compiler is now 3.8.0-RC4 with #24273 backported, it should unblock this PR

@natsukagami natsukagami changed the title Add missing lazy wrappers of KeySets for strict Maps Deprecate MapOps.KeySet, replace with private (and more performant) strict/lazy KeySets Jan 3, 2026
@natsukagami natsukagami changed the title Deprecate MapOps.KeySet, replace with private (and more performant) strict/lazy KeySets Deprecate MapOps.KeySet, replace with private (and more performant) strict/lazy KeySet implementations Jan 3, 2026
@natsukagami natsukagami marked this pull request as ready for review January 3, 2026 13:17
We need a data structure that both acts like a Set (queries membership efficiently), and has a stable iteration order.
LinkedHashSet seems to fit this criteria.
LinkedHashMap's `keySet` has always been returning a LinkedKeySet, a private data structure that was extended from the
generic MapOps.KeySet. Recently MapOps.KeySet was changed to a strict data structure, so we change the `keySet` method to
use the lazy wrapper instead. Unfortunately we cannot change the hierarchy of `LinkedKeySet`, but the API never said we have to use it.
…base implementation

This partially reverts `KeySet` and `GenKeySet` to their pre-capture-check implementation,
with some unsafe annotations and a deprecation notice. This should cause MiMa to be happy with
the previous implementations.
Copy link
Copy Markdown
Contributor

@WojciechMazur WojciechMazur left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me, but it would be nice to get second opinion from somebody more fluent with CC rules (@sjrd, @hamzaremmal)

It might be worth to create an issue for the remaining todo in collection.Map

@sjrd
Copy link
Copy Markdown
Member

sjrd commented Jan 6, 2026

more fluent with CC rules (@sjrd)

Ah, ah, I don't speak CC at all. 🤣

Comment on lines +319 to +322
def contains(key: K): Boolean =
// TODO: I'm not sure what's happening here, need to investigate a bit further.
// But this check should be fine: we of course can use a MapOps here.
caps.unsafe.unsafeDiscardUses(this.get(key)).isDefined
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@odersky could this be a compiler bug? Removing this unsafeDiscardUses will yield an use error on this, which is of course strange... But this error also only shows up after I added the unsafeDiscardUses on the current KeySet.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the logic in the compiler is quite simple, probably a problem with setup. You could try to add a print to where discardUses is checked in markFree and see whether it triggers.

@WojciechMazur WojciechMazur merged commit e49caf0 into scala:main Jan 7, 2026
56 checks passed
@WojciechMazur WojciechMazur added backport:accepted This PR needs to be backported, once it's been backported replace this tag by "backport:done" and removed backport:nominated If we agree to backport this PR, replace this tag with "backport:accepted", otherwise delete it. labels Jan 7, 2026
WojciechMazur added a commit that referenced this pull request Jan 7, 2026
…formant) strict/lazy KeySet implementations" to 3.8.0 (#24912)

Backports #24767 to the 3.8.0-RC5.

PR submitted by the release tooling.
@WojciechMazur WojciechMazur added backport:done This PR was successfully backported. and removed backport:accepted This PR needs to be backported, once it's been backported replace this tag by "backport:done" labels Jan 7, 2026
WojciechMazur added a commit that referenced this pull request Jan 13, 2026
…formant) strict/lazy KeySet implementations" to 3.8.1 (#24957)

Backports #24767 to the 3.8.1-RC1.

PR submitted by the release tooling.
[skip ci]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport:done This PR was successfully backported.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Map#keySet is slow in 3.8.0-RC3

7 participants