december adventure 2024
I’m going to try to work on something small everyday during december. see the original December Adventure.
my goal: work on sbt 2.x, other open source like sbt 1.x and plugins, or some post on this site, like music or recipe.
2024-12-10
bumping to Scala 3.6.2, and improving the compiler
since Scala 3.6.2 seems to have been released, I sent a PR to update sbt 2.x to Scala 3.6.2 in #7941.
not sure why there are no tweets from the official scala-lang.bsky.social and scala_lang@fosstodon.org. if you’re curious who are the people behind Scala 3, there’s a helpful Scala 3 Compiler Team page.
if you recall from day 5, Scala 3.6.x adds a new compiler warning related to the givens search prioritization change that is planned for Scala 3.7. today I discovered Upcoming Changes to Givens in Scala 3.7 post written by Oliver Bračevac in August 2024 detailing the change. as it turns out, the warning isn’t unactionable since we can suppress it by passing in -source 3.5
, -source 3.7
, or by filtering by the warning message:
import scala.annotation.nowarn
@nowarn("msg=Given search preference") // not great
val x = summon[A]
to commemorate the release of Scala 3.6 series, I’ve sent in a pull request scala/scala3#22189 ‘refactor: improve Given search preference warning’.
- this refactors the code to give the warning an error code
E205
. - when this is displayed as a warning, tell the user to choose
-source 3.5
vs3.7
, or use@nowarn("id=205")
annotation.
follow up on SbtParser ConcurrentModificationException
I sent #7938 last night as an attempt to fix a ConcurrentModificationException
, and wrote
since this wasn’t failing on CI, it’s hard to say if the fix would actually hold.
my trepidation about the fix was warranted, since overnight I got helpful code review by Adrien Piquerez that it won’t fix the concurrency issue by protecting the initialization per se.
Looking at the stack trace, it seems that dotty is iterating the
System.properties
while another thread modifies them.
João Ferreira also pointed out that in Scala 2.x compiler already implements a workaround for it in 2018 in scala/scala#6413. for now, I decided to wrap it in Retry(...)
, which should fix the issue in case we observe sys.prop
changes.
2024-12-09
a quick sbt 2.0.0-M2 bug fix today. about a month ago, xuwei-k reported #7873 ‘ConcurrentModificationException in SbtParser’:
Error: Exception in thread "sbt-parser-init-thread" java.lang.ExceptionInInitializerError
Error: at sbt.internal.parser.SbtParserInit$$anon$1.run(SbtParser.scala:179)
Error: Caused by: java.util.ConcurrentModificationException
Error: at java.util.Hashtable$Enumerator.next(Hashtable.java:1408)
....
Error: at dotty.tools.dotc.core.Contexts$ContextBase.<init>(Contexts.scala:857)
Error: at dotty.tools.dotc.Driver.initCtx(Driver.scala:61)
Error: at sbt.internal.parser.SbtParser$ParseDriver.<init>(SbtParser.scala:138)
Error: at sbt.internal.parser.SbtParser$.<clinit>(SbtParser.scala:135)
a few days ago João Ferreira reported that he’s seen it too. to parse build.sbt
DSL, sbt uses ligthtly customized Scala 3 compiler. for sbt 1.x, that’s Scala 2.12, and sbt 2.x it’s Scala 3.x. because the compiler JARs are hefty to JIT, we give it a head start by creating a thread to classload SbtParser
when sbt starts up.
/**
* This gives JVM a head start to JIT Scala 3 compiler JAR.
* Called by sbt.internal.ClassLoaderWarmup.
*/
private class SbtParserInit:
val t = new Thread("sbt-parser-init-thread"):
setDaemon(true)
override def run(): Unit =
val _ = SbtParser.defaultGlobalForParser
t.start()
end SbtParserInit
the problem is that if sbt starts up quickly enough, now the initialization might end up concurrency issue. I sent #7938 as an attempt to fix this:
+ private lazy val defaultGlobalForParser = ParseDriver()
+ private[sbt] def getGlobalForParser: ParseDriver = synchronized:
+ defaultGlobalForParser
....
+ val _ = SbtParser.getGlobalForParser
since this wasn’t failing on CI, it’s hard to say if the fix would actually hold.
2024-12-08
switching gear to sbt 2.0.0-M2 bug. let’s look into the exists
problem #7931, which is the top priority issue we need for 2.0.0-M3. one of the changes I made in sbt 2.x is the location of target
directory. in sbt 1.x each subproject has its own target
directory where the build artifacts like *.class
files and *.jar
files are created. in this model, the source code and binary directory are intertwined with each other. in sbt 2.x, there’s going to be one target
directory for the entire build, and each subproject would create a subdirectory under target
:
target/out/jvm/scala-3.5.2/root/classes/example/A.class
the problem is that now it’s difficult to write a scripted test that would work for both sbt 1.x and 2.x. a solution that I came up today is to allow glob support in the scripted test commands liks like exists
and absent
. for example the above can be tested as:
$ exists target/**/classes/example/A.class
the **
part would match to zero or more directories, so that should ignore the extra levels in sbt 2.x. a more tricky example might be:
# sbt 1.x
$ exists core/target/scala-3.5.2/classes/example/A.class
# sbt 2.x
$ exists target/out/jvm/scala-3.5.2/core/classes/example/A.class
for this, I’ve added ||
so we can write:
$ exists core/target/scala-3.5.2/classes/example/A.class || target/out/jvm/scala-3.5.2/core/classes/example/A.class
this would match either the path. given that ||
isn’t going to exist as a file name, this should be a safe change to introduce. we should still abstract out the Scala version so we don’t have to update it each time we bump the Scala version:
$ exists core/target/scala-3.*/classes/example/A.class || target/out/jvm/scala-3.*/core/classes/example/A.class
in scripted, the file commands are implemented in FileCommands.scala. glob functionality already exists in sbt 1.x as part of a change Ethan implemented for ~
improvements. first, we need to progress the arguments passed into exitst
into List[PathFilter]
:
def filterFromStrings(exprs: List[String]): List[PathFilter] =
def orGlobs =
val exprs1 = exprs.mkString("").split(OR)
.filter(_ != OR).toList.map(_.trim)
val combined = exprs1.map(Glob(baseDirectory, _)) match
case Nil => sys.error("unexpected Nil")
case g :: Nil => (g: PathFilter)
case g :: gs =>
gs.foldLeft(g: PathFilter) { case (acc, g) =>
acc || (g: PathFilter)
}
List(combined)
if exprs.contains("||") then orGlobs
else exprs.map(Glob(baseDirectory, _): PathFilter)
we can then pass this into FileTreeView.Ops(FileTreeView.default)
to see if the filter returns anything. is the returns is non-empty exists
succeeds, and if the result is empty absent
succeeds. PR for this is #7932.
skating notes
went skating in the afternoon a bit since it’s relatively nicer 11C/52F. more awkward penguin walks and monster walks. still trying to get used to ollie with AF-1. AF-1 is physically heavier, but what’s throwing me off literally might be more to do with timing of Indy Hollow vs AF-1. with Indy Hollow, I just needed to put some pressure upfront, and jump, and the pop happened on its own a split seconds later. with AF-1, part of the heaviness might just be unweighing timing. with AF-1 I sometimes jump up and I’m off the board, which likely means front leg needs to go up faster? on a positive note, when I can pop, it feels like the board comes up slower. if I can hang in the air, the perceived slowness could buy me time, potentially to a leveled out the ollie.
2024-12-07
went skating for a few hours in the evening. given that people go skiing and snow boarding in the mountain, I guess any temperature is skatable if you wear the right layers. my 4C/38F outfit was t-shirt, uniqlo flannel, big hoodie, lululemon jogger in a nice chino pants color, beanie hat, and a pair of thin gloves. after a while, I was running too warm and it was windy so switched hoodie with a marmot minimalist. put another way, skateboarding is snowboarding that you can do at your local empty park.
continuing from yesterday on the thin client. in case you didn’t know, sbt ships with a native thin client called sbtn, which can communicate with an existing sbt session. the motivation for the thin client is to reduce the startup speed (if the server is already up) and share the session with IDEs.
I posted the socat
dump of the UNIX domain socket: https://gist.github.com/eed3si9n/0e104e33caa18e468aab92af10dfaf28. in the session I issued compile
task. it might be surprising to see nearly 700 lines of log for passing in compile
. same as LSP, the first handshake is called initialize. next, we identified sbt/attach
method.
next, sbt server starts asking the thin client back for information:
{"jsonrpc":"2.0","id":"b8d058b0-af7a-4d40-93a9-5e2432183fe7","method":"sbt/terminalPropertiesQuery","params":""}
to this the thin client responds:
{ "jsonrpc": "2.0", "id": "b8d058b0-af7a-4d40-93a9-5e2432183fe7", "result": {"type":"TerminalPropertiesResponse","width":316,"height":43,"isAnsiSupported":true,"isColorEnabled":true,"isSupershellEnabled":true,"isEchoEnabled":true} }
sbt server then starts forwarding stdout:
{"jsonrpc":"2.0","method":"sbt/systemOut","params":[27,91,48,74]}
{"jsonrpc":"2.0","method":"sbt/systemOut","params":[27,91,50,75,27,91,49,48,48,48,68,27,91,48,74]}
{"jsonrpc":"2.0","method":"sbt/systemOut","params":[27,91,50,75,27,91,49,48,48,48,68,27,91,48,74]}
I typed the following to chatgpt:
Decode the following, assuming that the numbers represent ASCII code in decimal:
[27,91,48,74]
the response was:
ESC [ 0 J
This sequence is a common ANSI escape code used in terminal control. Specifically,ESC [ 0 J
clears the screen from the cursor to the end.
so far this all makes sense. then comes lines after lines of sbt/terminalCapabilities
method by sbt server:
{"jsonrpc":"2.0","id":"10a87b11-001f-407f-aa67-a48e6cead549","method":"sbt/terminalCapabilities","params":{"numeric":"max_colors"}}
{ "jsonrpc": "2.0", "id": "10a87b11-001f-407f-aa67-a48e6cead549", "result": {"type":"TerminalCapabilitiesResponse","numeric":256} }
{"jsonrpc":"2.0","id":"67296a90-e9b1-4dd9-af4b-8adf89f8556e","method":"sbt/terminalCapabilities","params":{"string":"key_a1"}}
{ "jsonrpc": "2.0", "id": "67296a90-e9b1-4dd9-af4b-8adf89f8556e", "result": {"type":"TerminalCapabilitiesResponse","string":"null"} }
{"jsonrpc":"2.0","id":"ac7e102d-2815-401e-8cc6-f490c3aa9a5a","method":"sbt/terminalCapabilities","params":{"string":"key_a3"}}
{ "jsonrpc": "2.0", "id": "ac7e102d-2815-401e-8cc6-f490c3aa9a5a", "result": {"type":"TerminalCapabilitiesResponse","string":"null"} }
{"jsonrpc":"2.0","id":"b846fbb7-6370-4a0b-8fde-47d5ea94ba2e","method":"sbt/terminalCapabilities","params":{"string":"key_b2"}}
{ "jsonrpc": "2.0", "id": "b846fbb7-6370-4a0b-8fde-47d5ea94ba2e", "result": {"type":"TerminalCapabilitiesResponse","string":"\\\\EOE"} }
....
{"jsonrpc":"2.0","id":"b90e4668-8d51-4e83-bf8f-8c958da17dd6","method":"sbt/terminalCapabilities","params":{"string":"exit_alt_charset_mode"}}
{ "jsonrpc": "2.0", "id": "b90e4668-8d51-4e83-bf8f-8c958da17dd6", "result": {"type":"TerminalCapabilitiesResponse","string":"\\\\E(B"} }
some more sbt/systemout
:
{"jsonrpc":"2.0","method":"sbt/systemOut","params":[27,91,63,49,104,27,61,27,91,63,50,48,48,52,104,115,98,116,58,102,111,111,27,91,51,54,109,62,32,27,91,48,109]}
according to chatgpt that’s:
ESC[?1h ESC= ESC[?2004h sbt:foo ESC[36m> ESC[0m
this is the sbt prompt with cyan >
.
sbt server then sends
{"jsonrpc":"2.0","method":"sbt/readSystemIn","params":""}
to prompt for stdin. sbtn sends
{ "jsonrpc": "2.0", "method": "sbt/systemIn", "params": 99 }
99
is c
for compile
. sbt server goes back to asking a few more the terminal capabilities and sends ‘c’ back in sbt/systemOut
method:
{"jsonrpc":"2.0","method":"sbt/systemOut","params":[99]}
basically it goes on to send compile
and \n
(CR). after a few stdout of ANSI control sequences, sbt server sends:
{"jsonrpc":"2.0","id":"06433732-e24f-4f6a-b278-a39a1b927f1b","method":"sbt/terminalSetRawMode","params":{"toggle":false}}
eventually we get stdout from some super shell outputs from the server:
{"jsonrpc":"2.0","method":"sbt/systemOut","params":[27,91,49,48,48,48,68,10,27,91,50,75,27,91,50,75,32,32,124,32,61,62,32,102,111,111,32,47,32,117,112,100,97,116,101,32,48,115,10,27,91,50,75,27,91,50,65,27,91,49,48,48,48,68]}
per chatgpt, that says:
| => foo / update 0s
next we kind of get a progress report from the server as well:
{"jsonrpc":"2.0","method":"build/taskStart","params":{"taskId":{"id":"2","parents":[]},"eventTime":1733553247768,"message":"Compiling foo","dataKind":"compile-task","data":{"target":{"uri":"file:/private/tmp/foo/#foo/Compile"}}}}
in any case, I think we get the idea of the style of communication between the thin client and sbt server. to put simply, it seems like Ethan has implemented telnet
/ ssh
equivalent over the existing JSON RPC protocol, including color support. let’s try uparrow.
{"jsonrpc":"2.0","method":"sbt/readSystemIn","params":""}
{ "jsonrpc": "2.0", "method": "sbt/systemIn", "params": 27 }
{"jsonrpc":"2.0","method":"sbt/readSystemIn","params":""}
{ "jsonrpc": "2.0", "method": "sbt/systemIn", "params": 79 }
{"jsonrpc":"2.0","method":"sbt/readSystemIn","params":""}
{ "jsonrpc": "2.0", "method": "sbt/systemIn", "params": 65 }
sbt server reponded as follows:
{"jsonrpc":"2.0","method":"sbt/systemOut","params":[99,111,109,112,105,108,101]}
chatgpt says it’s compile
, which mean up-arrow history works! in other words, the thin client faithfully reproduces the sbt shell experience including the history lookup and tab completions. we’ll continue tomorrow.
2024-12-06
an area of sbt that likely few people know the details about is the thin client, which was sort of prototyped first by me, but Ethan Atkins took it to the next level by supporting almost all tasks in a general way. let’s try reverse engineering sbtn to see how the native code is communicating with sbt 1.x.
the thin client is part of sbt 1.x’s code base, and it’s written in Scala 2.12. it is then compiled using GraalVM native-image to turn into a native app. it talks with sbt server, which uses JSON-RPC over a UNIX domain socket like an LSP server. to monitor the communication, first install socat.
start an sbt session in /tmp/foo
:
$ sbt
[info] Updated file /private/tmp/foo/project/build.properties: set sbt.version to 1.10.6
[info] welcome to sbt 1.10.6 (Azul Systems, Inc. Java 1.8.0_402)
.....
[info] sbt server started at local:///Users/xxxx/.sbt/1.0/server/aaaa/sock
[info] started sbt server
/Users/xxxx/.sbt/1.0/server/aaaa/sock
is the UNIX domain socket, the sbt server is listening. in another terminal, proxy the UNIX domain socket as follows:
$ socat -v UNIX-LISTEN:$HOME/.sbt/1.0/server/aaaa/proxy.sock,fork UNIX-CONNECT:$HOME/.sbt/1.0/server/aaaa/sock
next, open project/target/active.json
:
{"uri":"local:///Users/xxxx/.sbt/1.0/server/aaaa/sock"}
change the content to proxy.sock
instead:
{"uri":"local:///Users/xxxx/.sbt/1.0/server/aaaa/proxy.sock"}
open yet another terminal window in /tmp/foo
:
$ sbt --client
[info] entering *experimental* thin client - BEEP WHIRR
[info] terminate the server with `shutdown`
[info] disconnect from the server with `exit`
sbt:foo> compile
[success] Total time: 0 s
if you go back to the socat
window, the screen should be filled with JSON-RPC.
> 2024/12/07 01:30:49.000005239 length=181 from=0 to=180
Content-Length: 158\r
\r
{ "jsonrpc": "2.0", "id": "cb0ffdd8-be63-42cb-b853-4fce15a5c9f5", "method": "initialize", "params": { "initializationOptions": { "skipAnalysis" : true } } }\r
> 2024/12/07 01:30:49.000005821 length=148 from=181 to=328
Content-Length: 125\r
\r
{ "jsonrpc": "2.0", "id": "44b67b11-1551-424d-9b5f-1454112b769c", "method": "sbt/attach", "params": {"interactive": true} }\r
< 2024/12/07 01:30:49.000028766 length=338 from=0 to=337
Content-Length: 258\r
Content-Type: application/vscode-jsonrpc; charset=utf-8\r
\r
{"jsonrpc":"2.0","id":"cb0ffdd8-be63-42cb-b853-4fce15a5c9f5","result":{"capabilities":{"textDocumentSync":{"openClose":true,"change":0,"willSave":false,"willSaveWaitUntil":false,"save":{"includeText":false}},"hoverProvider":false,"definitionProvider":true}}}< 2024/12/07 01:30:49.000039588 length=192 from=338 to=529
....
this shows that sbtn sent initialize
method, and sbt/attach
method, and sbt serer responsed to the first request cb0ffdd8 with the list of capabilities supported by the server:
{"capabilities":{"textDocumentSync":{"openClose":true,"change":0,"willSave":false,"willSaveWaitUntil":false,"save":{"includeText":false}},"hoverProvider":false,"definitionProvider":true}}
full output is here https://gist.github.com/eed3si9n/0e104e33caa18e468aab92af10dfaf28. this looks promising. we’ll continue tomorrow.
2024-12-05
my two cents on compilers: compilers should be silent if it did exactly what was told. any warnings should be actionable such that the user can get rid of the warning somehow. -Xmigration
notices might be an exception. I feel like I’ve been saying this for years.
as a low effort exploration, I decided to try the next Scala 3.x, Scala 3.6.2-RC3. unfortunately the compilation failed under -Xfatal-warnings
because Scala 3.6.2-RC3 decided to display some warnings:
[warn] -- Warning: /xxx/sbt/protocol/src/main/contraband-scala/sbt/protocol/codec/SettingQuerySuccessFormats.scala:14:91
[warn] 14 | val value = unbuilder.readField[sjsonnew.shaded.scalajson.ast.unsafe.JValue]("value")
[warn] | ^
[warn] |Given search preference for sjsonnew.JsonReader[sjsonnew.shaded.scalajson.ast.unsafe.JValue] between alternatives
[warn] | (SettingQuerySuccessFormats.this.JValueFormat :
[warn] | sjsonnew.JsonFormat[sjsonnew.shaded.scalajson.ast.unsafe.JValue])
[warn] |and
[warn] | (SettingQuerySuccessFormats.this.JValueJsonReader :
[warn] | sjsonnew.JsonReader[sjsonnew.shaded.scalajson.ast.unsafe.JValue])
[warn] |will change.
[warn] |Current choice : the first alternative
[warn] |New choice from Scala 3.7: the second alternative
[error] No warnings can be incurred under -Werror (or -Xfatal-warnings)
[warn] two warnings found
[error] one error found
so in Scala 3.6 givens search prioritization is going to change, and we’re using compiler to announce this? setting aside the change itself, I think these “FYI - something WILL change” notification should go to -Xmigration:3.5.0
. if anyone uses Scala 3.x for a library or an app, they’ll look at this warning every time they compile the code. I submitted scala/scala3#22153 to file this as a bug.
sent PR #7928 to update the Scala CLA checker URL to https://contribute.akka.io/contribute/cla/scala/check/. note that the checker is hosted by Lightbend, Inc. dba Akka, but the CLA signs the rights away to EPFL for sbt code.
addressed one of review comments, from last night’s URI changes and landed #7927.
2024-12-04
sent a PR #7927.
java.net.URL
infamously calls out to the network to perform equals
, so likely we should avoid it for keys and data types that might be used in caching. thankfully not too many keys are URLs so I changed them all to URI.
related, I cherry picked a commit from a dormant PR that turns license information into a data type, as opposed to a tuple of (String, URL)
. I had a few backward compatibility suggestions in the PR, and I just implemented the suggestions myself.
2024-12-03
went skating in the morning before work. 8.25 inch + AF-1 still feels heavy compared to previous setups. the temperature was like 3C/37F going to 4C/39F. initially it was a bit cold, so I warmed up by pushing around the park then tictac, switch push, awkward penguin walks and monster walks on smooth surface. see Mike Osterman’s How to Monster Walk.
truck | weight |
---|---|
Tensor Aluminium 8" | 361 g |
Independent Stage 11 Hollow | 351 g |
Ace AF-1 44 | 393 g |
the above chart illustrates why AF-1 feels heavier to me. on the positive side, I’m exploring non-ollie tricks too so I should keep skating this setup a bit more.
no night hacking today, but I’ll document one of sbt 2.x bug fix that I implemented on day 1.
a couple months ago xuwei-k reported #7738. sbt has a semi-documented source-dependencies feature, and he’s found a bug in sbt 2.x which shows the project resolution doesn’t work:
[info] welcome to sbt 2.0.0-M2 (Eclipse Adoptium Java 21.0.4)
[info] loading project definition from /home/runner/work/sbt-2-ProjectRef/sbt-2-ProjectRef/project
java.lang.RuntimeException: Invalid build URI (no handler available): file:/home/runner/work/sbt-2-ProjectRef/sbt-2-ProjectRef/a1/a1/
to debug this I put in println(...)
in a bunch of places in Load.scala. it turned out that the problem was caused by the detection of whether a subproject is root project or not.
in Yoshida-san’s repro a/build.sbt
contained:
val a1 = (project in file("."))
so ProjectRef(file("a1"), "a1")
should have been resolved to the root project of the {file:/home/runner/work/sbt-2-ProjectRef/sbt-2-ProjectRef/a1}
build. this is one of the buggy lines:
- val (root, nonRoot) =
- rawProjects.partition(_.base.getCanonicalFile() == projectBase.getCanonicalFile())
in the above, projectBase
would have the absolute path of the build, and _.base
would be the base directory passed in by the user file(".")
. the problem is that for the source dependency situation, projectBase
is not the current directory of the sbt session. so file(".").getCanonicalFile()
becomes cwd (/home/runner/work/sbt-2-ProjectRef/sbt-2-ProjectRef/
), which doesn’t match projectBase
(/home/runner/work/sbt-2-ProjectRef/sbt-2-ProjectRef/a1/
)
the fix I sent in #7925 was to evaluate the base directory relative to projectBase
, and then compare the getCanonicalFile()
.
+ def isRootPath(value: File, projectBase: File): Boolean =
+ projectBase.getCanonicalFile() == IO.resolve(projectBase, value).getCanonicalFile()
....
+ val (root, nonRoot) = rawProjects.partition(p => isRootPath(p.base, projectBase))
2024-12-02
sent Artifact publishing proposal PR to Scala Center. not going to repeat the content here, but there’s been a number of changes to the landscape of publishing, but the solutions are worked on independently by the build tool silos, so I’ve been thinking it would be useful to consolidate the effort. this could start with basic things like generating correct ivy.xml
and pom.xml
, but also include more recent developments like bill-of-materials (BOM) support.
released sbt-jupiter-interface 0.13.3, featuring a bug fix contributed by Li Haoyi. sbt defines an interface for test frameworks, and sbt-jupiter-interface is an implementation for JUnit 5, not to be confused with Jupyter notebook.
worked on december mixtape at night. 3h assortment of electronica for taking a walk or skating.
2024-12-01
looking at sbt 2.x bugs that’s been reported against 2.0.0-M2.
#7723 reported by xuwei-k (Kenji Yoshida). it says that on sbt 2.x you get a compiler warning during load “Failed to parse -Wconf configuration: cat=unused-nowarn:s”. first, this indicates that Scala 3.3.x or 3.5.x isn’t really compatible with Scala 2.x’s -Wconf
flag. the flag was ported, but the category part is completely different in Scala 3. to unwind why sbt 1.x even has this flag,
- in 2020, we observed #6161 “a pure expression does nothing” warning because Scala 2.12.12 started to be more strict about detecting pure expressions
- as a workaround in 2021, I added
(??? :@scala.annotation.nowarn("cat=other-pure-statement"))
around build.sbt macro expansions - during 1.5.0 RC-1, we observed that in some cases
nowarn
itself would cause additional warning #6398 “@nowarn annotation does not suppress any warnings” - as a workaround to the workaround,
"-Wconf:cat=unused-nowarn:s"
was added in #6403
given that Scala 3.x hasn’t started to warn about pure expressions, I can remove the workaround, which is what I did today in #7924.
earlier during the day, I was staring at VisualVM heap drump. Adrien Piquerez reported it in one of his pull requests, but seeing ScopedKey(...)
occupies 8% of the heap was jarring. I didn’t remember that he’s already tried interning it and saw not much difference, so I tried it and saw not much difference. perf optimization is sometimes like that.