Skip to content

feat: Implement granular compile-time caching for LightTypeTag in Scala 3#569

Open
AriajSarkar wants to merge 12 commits into
zio:developfrom
AriajSarkar:feature/350-scala3-compile-time-cache
Open

feat: Implement granular compile-time caching for LightTypeTag in Scala 3#569
AriajSarkar wants to merge 12 commits into
zio:developfrom
AriajSarkar:feature/350-scala3-compile-time-cache

Conversation

@AriajSarkar

Copy link
Copy Markdown
Contributor

/claim #350

Implemented

  • LTT-level cache (Inspect.scala)
  • FullDb cache (FullDbInspector.scala)
  • InheritanceDb cache (InheritanceDbInspector.scala)
  • SoftReference wrapping + ConcurrentHashMap
  • Single on/off flag via izumi.reflect.rtti.cache.compile
  • Compile-time benchmarks (see issue350/compile-time-benchmarks.md)

Benchmark Results

Scala Version Improvement
Scala 3.3.6 2.3% faster
Scala 2.13.14 6.6% faster

Not yet done

Item Reason
TagMacro-level cache Lower priority; focused on LTT-level first
Separate on/off flags per cache Single flag for now; individual flags can be added after feedback

Submitting for early review to confirm direction before additional work.

Note: The issue350/ folder is for review context only and will be removed before merge.

Fixes #350

Implements three-level compile-time caching for both Scala versions:
- LTT cache: Caches complete LightTypeTag results
- FullDB cache: Caches inheritance hierarchy (basesDb)
- InheritanceDB cache: Caches unapplied type inheritance data

Scala 3 changes:
- Inspect.scala: LTT-level cache with structural key generation
- FullDbInspector.scala: FullDB-level cache
- InheritanceDbInspector.scala: InheritanceDB-level cache

Scala 2 changes:
- LightTypeTagImpl.scala: All three cache levels with structural key

Cache implementation details:
- Uses ConcurrentHashMap with SoftReference for memory efficiency
- Structural key normalization ensures type equivalence across contexts
- Controlled via -Dizumi.reflect.rtti.cache.compile property (default: true)
- Cache statistics available via getCacheStats/resetCacheStats methods

New configuration properties in DebugProperties.scala:
- izumi.reflect.rtti.cache.macro
- izumi.reflect.rtti.cache.ltt
- izumi.reflect.rtti.cache.db

Added comprehensive tests for both Scala versions:
- CacheStressTest.scala: Stress tests with repeated Tag materializations
- CompileTimeCacheBenchmarkTest.scala: Correctness verification tests

Benchmark results:
- Scala 3: ~1% clean build improvement, ~66% warm build improvement
- Scala 2.13: ~9% clean build improvement, ~64% warm build improvement
Implements three-level compile-time macro caching:
- LTT cache: Caches final LightTypeTag results
- FullDB cache: Caches full database computations
- InheritanceDB cache: Caches inheritance database computations

Key features:
- Uses ConcurrentHashMap with SoftReference wrappers for memory efficiency
- Structural cache keys using type representation + base classes with source position
- Source position (file path + byte offset) ensures unique keys for local types
- Configurable via izumi.reflect.rtti.cache.compile property (default: true)

Benchmark results (clean compilation):
- Scala 3.3.6: 49.07s -> 47.95s (2.3% faster)
- Scala 2.13.14: 43.90s -> 41.02s (6.6% faster)

Note: issue350/ folder contains benchmark documentation for review only
…erties

Cleanup:
- Removed cacheHits/cacheMisses counters and getCacheStats/resetCacheStats methods
- Removed unused config properties (cache.macro, cache.ltt, cache.db) from DebugProperties
- Simplified cache lookup code using getOrElse pattern

The cache stats were only useful for debugging and add unnecessary overhead.
Only the essential izumi.reflect.rtti.cache.compile property is needed.
@pshirshov

Copy link
Copy Markdown
Member
  • We need individual flags for every type of cache, we need to figure out which caches are the most beneficial
  • Try to directly use compiler structures (e.g. Type) as cache keys, avoid constructing serialized string keys - that comes with significant performance penalty.

Overall, 2..6% improvement doesn't look significant.

  • Try to introduce tagmacro-level cache, that might help for some cases where we generate multiple instances of identical tags.
  • Try to cache serialized binary form of the tag, I hope that it could give us something better than sub 10-% improvement.

- Add individual cache flags (ltt, db.full, db.inheritance, macro)
- Remove unused PickleImpl import from Inspect.scala
- Add benchmark scripts for reproducible testing
- Update .gitignore for generated stress test files
@AriajSarkar

Copy link
Copy Markdown
Contributor Author

Updated Implementation

Addressed all feedback from the previous review. Here are the latest benchmark results:

Benchmark Results

Scala Version With Cache Without Cache Improvement
Scala 2.13.14 41.85s 45.56s 8.1%
Scala 3.3.6 46.69s 52.84s 11.6%

Environment: Windows 11, i5-12500H, JDK 17, sbt 1.11.2

Key Implementation Notes

Scala 3 uses String-based cache keys because TypeRepr is path-dependent on Quotes and cannot be stored across macro invocations. This is a fundamental limitation documented in the Scala 3 Macro Reflection guide. See Inspect.scala line 82-92 for details. And the compile-time-benchmarks.md

Scala 2 uses Type directly as cache keys via WeakHashMap.

How to Reproduce

# Generate stress test (1755 Tag materializations)
.\scripts\generate-stress-test.ps1
# Run benchmark
.\scripts\benchmark-compile-time.ps1

Request

@pshirshov @neko-kai Could you please test on your machines as well? I'm curious about performance differences across CPU architectures (especially on macOS/Linux). The numbers above are from Windows on Intel 12th gen hybrid architecture.

@neko-kai

Copy link
Copy Markdown
Member

@AriajSarkar Re: path dependency of Quotes, that's not an issue, much less a "fundamental" one, just use a type projection e.g. Quotes#reflectModule#TypeRepr as key type

@AriajSarkar

Copy link
Copy Markdown
Contributor Author

Thank you for the insight Sir @neko-kai! I wasn't aware type projections could work here i'll look into changing the cache key from String to Quotes#reflectModule#TypeRepr and update the PR. Would using TypeRepr directly as the key provide better performance than the current string-based approach?

@pshirshov

Copy link
Copy Markdown
Member

It should because you won't need to serialize.

@AriajSarkar

Copy link
Copy Markdown
Contributor Author

I may have overthought this earlier! 😅 Made the change to use Quotes#reflectModule#TypeRepr as suggested and got these results:

Scala Version WITH Cache WITHOUT Cache Improvement
Scala 3.3.6 33s 39s ~15%
Scala 2.13.14 29s 37s ~22%

Thanks for the tip - learned something new about type projections!

@pshirshov

Copy link
Copy Markdown
Member

Ok, could you provide results for individual cache types?

@AriajSarkar

Copy link
Copy Markdown
Contributor Author

Scala 3.3.6:

Config Time Improvement
ALL OFF 50.1s baseline
ALL ON 43.9s +12.4%
LTT only 43.7s +12.8%
FullDB only 44.7s +10.8%
InheritanceDB only 44s +12.2%
Macro/Serialized only 45.8s +8.6%

Scala 2.13.14:
Baseline (ALL OFF): 39.3s

ALL ON 41.2s ( -4.8%)
LTT only 39.9s ( -1.5%)
FullDB only 40.2s ( -2.3%)
InheritanceDB only 40.4s ( -2.8%)
Macro/Serialized only 40.7s ( -3.6%)

Scala 2 shows slight overhead - should we disable caching by default for Scala 2, or is this expected for a small test suite?

- Use Quotes#reflectModule#TypeRepr as cache key instead of String
- Remove makeTypeKey function (no longer needed)
- Clean up comments
- Remove benchmark files and stress tests (not needed for PR)
@AriajSarkar

AriajSarkar commented Dec 21, 2025

Copy link
Copy Markdown
Contributor Author

Scala 3 shows consistent ~12% improvement across cache types. Scala 2 shows slight overhead on this test suite.
Caches are enabled by default - users can disable with -Dizumi.reflect.rtti.cache.compile=false.

Again test -
Baseline (ALL OFF): 54.9s

ALL ON 55.5s ( -1.1%)
LTT only 39.2s ( 28.6%)
FullDB only 46.6s ( 15.1%)
InheritanceDB only 40s ( 27.1%)
Macro/Serialized only 40.4s ( 26.4%)

Note: The previous String-based key approach showed improvement for both Scala versions (~15% Scala 3, ~22% Scala 2). The new TypeRepr projection approach is cleaner but performs better only for Scala 3, with slight overhead on Scala 2 for this test suite.

@neko-kai

neko-kai commented Dec 22, 2025

Copy link
Copy Markdown
Member

@AriajSarkar
NOTE: Another thing we might want to check is caching the entire tree of derived LTT like in https://github.com/7mind/izumi/pull/2325/files#diff-f00f65085d86475ab54da4feff8fd89888f1bab667b561e7a3e436c50963d9e8R35 - you'll also need to make sure that each term in the tree is wrapped into a Typed node (in quasiquotes a Typed node is generated via a type ascription) as in 7mind/izumi#2331 - that should fully avoid typechecking of the tree on Scala 3 (and the LTT inspection itself). On Scala 2 avoiding type checks requires using internal APIs, it's harder to do than on Scala 3, but may be doable too given the very small tree we produce (at least for LTT case) just caching result of c.typecheck - that's a fully typed tree that will not cause any more type checks when reused, it's actually easier to avoid typechecking for our usecase on Scala 2 than 3.
NB2: Actually, no, I was wrong. Typed nodes are not sufficient to fully remove typechecking, compiler APIs have to be used anyway (either pre-typechecking with dotty.tools.dotc.typer.Typer#typed or manually setting types for ALL nodes with dotty.tools.dotc.ast.Trees.Tree#withTypeUnchecked). – pre-typechecking is certainly more robust.
NB3: Actually, no, I was wrong. Seems like no action is really required to set types for dotty trees, as they're typed at construction and ought to be cacheable as-is.

Re: benchmarks, something is wrong with your setup as the results vary a lot between your posts with seemingly no changes. You should probably first make sure that you have Turbo Boost or similar dynamic frequency control disabled on your PC and also run way more iterations per test (1000+)

@pshirshov

pshirshov commented Dec 23, 2025

Copy link
Copy Markdown
Member

It might be difficult to run multiple iterations in the regular sense, because the nature of the benchmark assumes invoking the compiler and the startup overhead might be significant.

I would suggest to generate sufficiently large test case file with enough duplication.

The variations in the results (+22% vs -3.6% for the same Scala 2 test) suggest that something is wrong with your environment or methodology.

I would suggest:

  1. Make sure you run the compiler with enough heap. If the compiler gets stuck in GC, the test results are worth nothing
  2. Try to monitor GC parameters using, for example, jVisualVM
  3. Make sure your CPU governor is not affecting the results (but that's actually unlikely to trigger such significant variations). If you use laptop, only run benchmarks while connected to the charger.

@AriajSarkar

AriajSarkar commented Dec 23, 2025

Copy link
Copy Markdown
Contributor Author

Thank you for the detailed feedback! I'm looking into the tree-level caching approach from #2331.

For the benchmark variance - I'm running on a laptop (charger connected(although it always plugged)). Will try:

  • Larger JVM heap (-Xmx4G)
  • Larger test file with more duplications
  • Multiple runs to get more consistent results

Will report back with improved methodology.


Edit:
Scala 2:
Caching c.typecheck result - fully typed tree, no re-typing on reuse.

Scala 3:
Caching via Term cast (ref: #2325). Store as Quotes#reflectModule#Term, cast back with asInstanceOf[qctx.reflect.Term].
Per NB3, no Typed wrapping needed - Dotty trees are typed at construction.

Benchmarks (3000 tags, -Xmx4G, warmup):

Avg of 3 runs both version.

Version Cache ON Cache OFF
Scala 3 52.9s 53.9s
Scala 2 43.3s 43.7s

~2% improvement. Modest gains in high-heap env, but architecture matches your request.

- Scala 2: Cache c.typecheck results in a synchronized WeakHashMap to avoid re-typing.
- Scala 3: Cache underlying Terms with path-independent types (Term-cast technique) to bypass ScopeException and avoid re-typing.
- Verified with benchmarks showing 1-2% compile-time improvement in high-heap environments.
@neko-kai

Copy link
Copy Markdown
Member

As @pshirshov mentioned above, you have to construct a specific test file that would provoke many cache hits. e.g. Compilation time of a file with thousands of lines of derivations for a tag for the same custom case class ought to be significantly improved by caching. Either previous Scala 2 cache or your new caches or a tree-based cache or at the very least just avoiding macro call by storing the derived tag in implicit val: implicit val tagForCustomTagCaseClass: Tag[CustomCaseClass] = Tag.tagFromTagMacro[CustomCaseClass] – all of these should have a very significant effect, at the very least the last one should have a dramatic effect by avoiding macro calls altogether. The goal then is to speed-up, via caching, the compilation time of such a file, until it approaches, as closely as possible, the version of that file with the tag cached via an implicit val.

@AriajSarkar

Copy link
Copy Markdown
Contributor Author

Sir @neko-kai, ran the benchmark you requested - comparing macro calls vs implicit val baseline for a custom case class.

Simple Case Class - 3000 Tag calls

Type: case class CustomCaseClass(id: Int, name: String, value: Double)

Scenario Compile Time
BASELINE (implicit val, no macros) 30s
WITH CACHE (macro calls) 36s
WITHOUT CACHE (macro calls) 36s

Cache doesn't help for simple types - both WITH and WITHOUT cache take same time.

Why?

Verified cache IS hitting (added debug output, saw cache hits). The problem is the tag computation for simple case classes is already fast - the overhead comes from Scala 3's inline macro expansion which runs every time regardless of caching.

Also tested complex types - 500 Tag calls

Tried with deeply nested generics (inheritance hierarchies, multiple type params, type lambdas):

Scenario Compile Time
WITH CACHE 32s
WITHOUT CACHE 35s

~10% improvement here. Complex types have expensive LightTypeTag computation so caching helps. But that's not what you asked for.

Commands I used

# WITH CACHE
$env:SBT_OPTS = "-Xmx4G"
sbt clean
sbt "++3.3.6" "izumi-reflectJVM/Test/compile"

# WITHOUT CACHE  
$env:SBT_OPTS = "-Xmx4G -Dizumi.reflect.rtti.cache.compile=false"
sbt clean
sbt "++3.3.6" "izumi-reflectJVM/Test/compile"

Test file structure

case class CustomCaseClass(id: Int, name: String, value: Double)

// BASELINE - no macro calls after initial derivation
object BaselineWithImplicitVal {
  implicit val cachedTag: Tag[CustomCaseClass] = Tag[CustomCaseClass]
  val t1 = implicitly[Tag[CustomCaseClass]]
  val t2 = implicitly[Tag[CustomCaseClass]]
  // ... 3000 total
}

// MACRO CALLS - each is a macro expansion
object MacroCallsWithCache {
  val t1 = Tag[CustomCaseClass]
  val t2 = Tag[CustomCaseClass]
  // ... 3000 total
}

Bottom line

For simple case classes, macro calls can't approach the implicit val baseline because the inline expansion overhead dominates, not the tag computation. Cache works and hits correctly, but there's nothing expensive to cache.

Note: Benchmarks run on Windows 11. Absolute numbers may vary but the relative comparison (WITH vs WITHOUT on same run) should be valid. Happy to have someone verify on a dedicated benchmark setup.

Let me know what you think or if you want me to try something different!

@neko-kai

neko-kai commented Jan 1, 2026

Copy link
Copy Markdown
Member

@AriajSarkar Could you just commit reproducible benchmark files into the branch, for manual checking?

AriajSarkar and others added 2 commits January 2, 2026 01:55
- Remove version-specific CacheStressTest2.scala and CacheStressTest3.scala
- Create izumi-reflect-stress-baseline module (implicit val resolution benchmark)
- Create izumi-reflect-stress-macrocalls module (Tag macro cache benchmark)
- Both modules are cross-version and unpublished (benchmark-only)

🤖 Generated with Claude Code
@neko-kai

neko-kai commented Jan 2, 2026

Copy link
Copy Markdown
Member

@AriajSarkar Your benchmark was flawed. I committed a fix just now. I'm getting 3 second compile time with implicit val (izumi-reflect-stress-baselineJVM / compile), 13 second without cache, 10 second with cache (izumi-reflect-stress-macrocallsJVM / compile), all on 2.13 ;

@AriajSarkar

Copy link
Copy Markdown
Contributor Author

@neko-kai Thanks Sir, for fixing the benchmark setup! Good to see the cache actually provides ~23% improvement 🎉

So the implementation was right, just my benchmarking methodology needed work 😅 , having both objects in the same file meant compiling everything twice instead of testing them separately.

(My Windows 11 still pulling 82s though 🙃)

Anything else needed from my side, or is this ready for final review?

P.S. Nice to see LLMs getting some love 🫠

@neko-kai

neko-kai commented Jan 2, 2026

Copy link
Copy Markdown
Member

@AriajSarkar No, it's not ready - there are multiple caching strategies present, but we don't know which of them actually work (hit), nor which are more effective than, or subsume, others. Also, manually running sbt compile once or twice, especially, in my case, on a a laptop, doesn't give us an accurate performance picture - I suggest looking at how compiler benchmarks for https://github.com/scala/scala3 are made and to see if something can be lifted from there. Also, I would suggest to read https://bcantrill.dtrace.org/2025/12/05/your-intellectual-fly-is-open/ and avoid writing replies directly with LLMs or pasting their output raw, without delineating from your own writing - it's jarring to read and not as helpful as the verbosity of their output would suggest. In fact I just skip reading everything that looks pasted from an LLM because it's too much work to figure out whether I'm reading something that's supposed to make sense or a nonsensical hallucination.

@AriajSarkar

Copy link
Copy Markdown
Contributor Author
=== izumi-reflect compile-time cache stats ===
termCache:        hits= 65999  misses=     1
lttCache:         hits=     0  misses=     1
serializedCache:  hits=     0  misses=     1
fullDbCache:      hits=     0  misses=     1
inheritanceDbCache: hits=     0  misses=     1
===============================================

the term cache (tree-level, hit ratio is 99.9985%) handles all reuse.
lower-level caches dont hit because term cache itercepts first.

@neko-kai
Q. should i remove the redundant caches (ltt, serialized, fulldb, inheritance) since termcache makes them unreacheable?
or keep them as fallback for cases where tree caching might fail?

Want me to push the instrumention? so you can run it yourself and verify?

cache disabled run:

$env:JAVA_OPTS="-Dizumi.reflect.rtti.cache.compile.stats=true -Dizumi.reflect.rtti.cache.compile=false"; sbt "izumi-reflect-stress-macrocallsJVM/clean" "izumi-reflect-stress-macrocallsJVM/compile"

stats:

=== izumi-reflect compile-time cache stats ===
termCache:        hits=     0  misses=     0
lttCache:         hits=     0  misses=     0
serializedCache:  hits=     0  misses=     0
fullDbCache:      hits=     0  misses= 66000
inheritanceDbCache: hits=     0  misses= 66000
===============================================

Benchmark results :

Scenario Time Cache Stats
Baseline (implicit val) 15s n/a
WITH cache 31-33s termCache: 65999 hits, 1 miss
WITHOUT cache 35-37s fullDb/inheritanceDb: 66000 misses each

re: proper benchmarks - Scala/Scala3 uses JMH via the bench/ directory. That’s the correct approach for statistically valid measurements. I can try set this up to validate the cache behavior properly.

@neko-kai

neko-kai commented Jan 3, 2026

Copy link
Copy Markdown
Member

The other caches should hit in other scenarios - e.g. LTT cache should hit when CustomCaseClass is inspected as a type parameter of another type. DB caches should hit when it's a base type of another type; they ought to be relevant for inheritors of Scala collection types, which have very large inheritance hierarchies.
re: JMH. Yes, copy their approach. If they're using JMH, I suppose they call the compiler entrypoint directly in jvm instead of running the binary - otherwise there's no point in using JMH - you should be able to replicate that by adding dependency on scala3-compiler

@AriajSarkar

Copy link
Copy Markdown
Contributor Author

@neko-kai

Tested type parameter and collection inheritance scenarios.
Results:

termCache:        hits=75715  misses=85
lttCache:         hits=0      misses=85
serializedCache:  hits=0      misses=85
fullDbCache:      hits=0      misses=85
inheritanceDbCache: hits=0    misses=85

DB caches don't hit because they're keyed by full type (List[Int]), not component types. When inspecting List[Int] vs Vector[Int], the shared Iterable ancestry isn't a separate cache lookup - it's computed inside buildFullDb.

termCache intercepts first for any same-type reuse. The other caches only help if we had different TypeReprs producing identical LightTypeTags, which doesn't happen in practice.

you can verify with:

JAVA_OPTS="-Dizumi.reflect.rtti.cache.compile.stats=true" sbt "izumi-reflect-stress-macrocallsJVM/compile"

Re: JMH - still needed, or is termcache data sufficient?

@neko-kai

neko-kai commented Feb 6, 2026

Copy link
Copy Markdown
Member

@AriajSarkar
Then the other caches aren't properly implemented, in your suite

    val t15_1 = implicitly[Tag[Base]]
    val t15_2 = implicitly[Tag[Level1]]
    val t15_3 = implicitly[Tag[Level2]]
    val t15_4 = implicitly[Tag[Level3]]
    val t15_5 = implicitly[Tag[Leaf1]]
    val t15_6 = implicitly[Tag[Leaf2]]
    val t15_7 = implicitly[Tag[Leaf3]]
    val t15_8 = implicitly[Tag[Leaf4]]
    val t15_9 = implicitly[Tag[Service]]
    val t15_10 = implicitly[Tag[DatabaseService]]
    val t15_11 = implicitly[Tag[CacheService]]
    val t15_12 = implicitly[Tag[ApiService]]
    val t15_13 = implicitly[Tag[PostgresService]]
    val t15_14 = implicitly[Tag[RedisService]]
    val t15_15 = implicitly[Tag[RestApiService]]
    val t15_16 = implicitly[Tag[List[Base]]]
    val t15_17 = implicitly[Tag[List[Level1]]]
    val t15_18 = implicitly[Tag[List[Level2]]]
    val t15_19 = implicitly[Tag[List[Level3]]]
    val t15_20 = implicitly[Tag[List[Leaf1]]]
    val t15_21 = implicitly[Tag[List[Leaf2]]]
    val t15_22 = implicitly[Tag[List[Service]]]
    val t15_23 = implicitly[Tag[Option[Base]]]
    val t15_24 = implicitly[Tag[Option[Leaf1]]]
    val t15_25 = implicitly[Tag[Option[Service]]]
    val t15_26 = implicitly[Tag[Map[String, Base]]]
    val t15_27 = implicitly[Tag[Map[String, Service]]]

Base, Level*, Leaf, List are repeated in different positions. One would expect the LTT cached Base to be reused when deriving List[Base], even if afterwards all repetitions of List[Base] are cached by termCache.

@AriajSarkar

Copy link
Copy Markdown
Contributor Author

yea you're right - the DB caches weren't checking for cached component types during recursive processing.

now checking the cache inside inspectTypeReprToFullBases and inspectTypeReprToUnappliedIndirectBases for each component type, not just at the top-level entry point.

$env:JAVA_OPTS="-Dizumi.reflect.rtti.cache.compile.stats=true"; sbt "++3.3.6" "izumi-reflect-stress-macrocallsJVM/clean" "izumi-reflect-stress-macrocallsJVM/compile"

Results -

=== izumi-reflect compile-time cache stats ===
termCache:        hits= 75715  misses=    85
lttCache:         hits=     0  misses=    85
serializedCache:  hits=     0  misses=    85
fullDbCache:      hits=   318  misses=    85
inheritanceDbCache: hits=   297  misses=    85
===============================================
sbt "++3.3.6" izumi-reflectJVM/test
=== izumi-reflect compile-time cache stats ===
termCache:        hits=   775  misses=   665
lttCache:         hits=     0  misses=   665
serializedCache:  hits=     0  misses=   674
fullDbCache:      hits=  6477  misses=   665
inheritanceDbCache: hits=  1104  misses=   665
===============================================

@AriajSarkar

Copy link
Copy Markdown
Contributor Author

@neko-kai Three caches working, two are dead:

lttCache:            hits=0      misses=85
serializedCache:     hits=0      misses=85
  • termCache - dominant, 99.9% hit rate, stores the full generated Term
  • fullDbCache - complementary, hits some, reuses component DBs across types (e.g. cached Base DB reused when computing List[Base])
  • inheritanceDbCache - same pattern

lttCache never hits because termCache uses the same key and is checked first. serializedCache never hits because it uses IdentityHashMap, each fresh LightTypeTag is a new object so identity comparison never matches.

should I remove lttCache and serializedCache (and their flags), keeping only the 3 that work?

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.

Granular complile-time caches for Scala 2 and 3

3 participants