FAQ
Why is this necessary? Couldn't Scala 3 just support Scala 2 macros?
No.
Macros are closely tied to the compiler's internals. Scala 2 and Scala 3 have different compilers with different internals, and such a translation layer could be more complex than either of the compilers (if anyone would be willing to fund it).
Hearth is possible, because:
- it's not a translation layer, it's one high-level abstraction over 2 low-level APIs
- it's not aiming to handle all possible cases, only the common ones
- it's resorting to some "unprincipled" (but well thought out and tested) hacks under the hood, which will fail in some cases - it might be seen as a "non-elegant" solution that occasionally requires a workaround - but by not promising perfection it immediately unblocks people
Do I need this library if I don't want to cross-compile?
It depends on how complex your macro is.
If your macro only needs to quote and unquote values — with no programmatic logic for generating the quoted code — you
might not need macros at all. Scala 3's inline defs, inline match, scala.compiletime utilities and
scala.deriving.Mirror can handle many use cases without ever touching the macro API directly. Scala 2's quasiquotes
similarly cover simple interpolation-style code generation reasonably well on their own.
However, once your macro needs to inspect types, traverse fields, match on type structure, or construct ASTs programmatically — anything beyond straightforward quoting — you are working with the compiler's reflection API. And that is where things get difficult:
- Scala 3's
quoted.reflect(aka TASTy Reflect) is a large, complex API that mirrors the compiler's internal tree representation. Using it effectively requires understanding compiler internals: how trees are typed, how symbols relate to definitions, how ownership works, and how to navigate the AST without breaking invariants. - Scala 2's macro API (
scala.reflect.macros/scala.reflect.api) has similar complexity with its own set of footguns: universes, tree attachments, positions, manual type application, and the ever-present risk of accidentally creating untyped trees. - Both APIs are poorly documented, have surprising edge cases, and produce cryptic error messages when something goes wrong.
For any macro that goes beyond a toy example, this complexity adds up quickly. Hearth tries to absorb as much of that burden as possible by providing high-level, tested abstractions over these low-level APIs — so you can focus on what your macro should generate rather than how to convince the compiler to accept it.
There are also many gotchas even for common cases:
- type parameters aren't automatically applied; if a class has type parameters they need to be reapplied to its methods and child subtypes
- constructing pattern-matching on Scala 3 requires building the AST from scratch
- quasi-quoting expressions on Scala 2 offers no help from the IDE since all it sees is
an interpolated
String - constructing names for
vals,vars,lazy vals,defs, bindings in pattern-matching... requires "fresh name generation" to avoid accidental name conflicts - providing a healthy architecture of a macro — with error handling, user-friendly debugging, composing code with FP patterns known from other ecosystems — would feel discouraging if one had to reimplement all the utilities from scratch for each new macro
All of the above and more contribute to bad UX for macro maintainers and discourage keeping the same good practices that we use in other Scala projects. Providing a set of ready-to-use utilities helps make macro development sane.
Do I need to use all of the utilities?
No. You can use only the ones that you need.
The dependencies between the modules are:
Core (Basic Utilities)
┃
┏━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━┓
┃ ┃ ┃
Better Printers Micro FP Cross Quotes
This modular design allows picking only what you want to use:
- you can use Better Printers without using the rest of the utilities if you only want better output
- you can use Micro FP if you only want to use a few type classes and/or Macro IO
- you can use Basic Utilities without explicitly depending on Better Printers or Micro FP
- while core tries to not force you into FP-style, many utilities provide type class instances allowing
e.g.
.map,.traverse,.parTraverse, but also type classes provide instances for:Id,Either,Option, ... to allow you working with them without any new collection types - Cross Quotes are completely optional; they are encouraged if you want to build for both Scala 2 and Scala 3, but if you only want to target one of them, you can work with quasi-quotes/quotes/ASTs directly
- while core tries to not force you into FP-style, many utilities provide type class instances allowing
e.g.
Utilities work best if used together, but it's easier to learn one tool at a time and reach for the next one only once we stumble upon the problem that a new tool would solve.
Does this library completely eliminate the need to learn macros?
No. Its goal is to handle the most common cases with a sane API, but it's impossible to cover all cases.
However, it should be able to provide enough utilities, that many macros could be implemented using only the Hearth's APIs. And for remaining cases it could allow using a sane API for the majority of the time, reaching for low-level (native) ASTs in one place, and keeping everything high-level everywhere else.
To implement these use-case-specific utilities you would have to know the macro API for the Scala version, that you are working with, but since there should be only a small portion of the whole codebase, it would be easier to test them (since you should be able to trust the behavior of the existing utilities).
Are there any examples of real-world macros built with Hearth?
Yes — Kindlings.
Kindlings is a companion repository that re-implements several real-world derivation macros using Hearth. It serves multiple purposes:
- Primary test bed — every Hearth utility is battle-tested through Kindlings. If a feature works in Kindlings' derivations, it works in practice. If something is missing or broken, Kindlings is usually where it surfaces first.
- Feature discovery — Kindlings exercises what is possible today, reveals what is not yet implemented, and identifies which features are missing from Hearth.
- Quality bar — we strive to provide at least as good a user experience as the original libraries. When possible, we aim to do better — leveraging Hearth's abstractions to produce clearer error messages, handle more edge cases, or simplify the user-facing API compared to the original implementations.
If you want to see how Hearth looks in a non-trivial macro codebase, Kindlings is the place to start.
Is Cross-Building a macro supported?
No.
Scala 3 supports a concept described as cross-building a macro.
What it means is that in Scala 3 code one is able to write:
object Macros {
// this will be called only by Scala 2.13
def macroMethod = macro scala2macroMethodImpl
// this will be called only by Scala 3
inline def macroMethod = ${ scala3macroMethodImpl }
}
and then compile and publish it as a Scala 3 artifact, and it will expand macros in both Scala 3 and Scala 2.13 (using this artifact via Cross.for2_13Use3).
The catch is that:
- both
scala2macroMethodImplandscala3macroMethodImplhave to be available wheremacroMethodis defined - Scala 3 cannot compile the Scala 2's quasi-quotes (it can compile manually written ASTs)
- you cannot have on the classpath two versions of the same class - e.g. compiled once by 2.13 and once by 3
In practice it makes it virtually impossible to use when you need to provide a type class derivation via the companion object:
- the companion object would have to provide both
def macroandinline def - that means two non-abstract, stable definitions must exist, one with the compiled macro for Scala 2.13 and one for Scala 3
- if quasi-quotes are necessary (most of the time they are), the Scala 2.13 macro code would have to be moved to another module
- inside the macro you almost certainly need a
c.Typedefinition of the type class - that has to be defined in the same file as its companion object, which would live in the module that depends on the module containing the macro
- even if you find a creative workaround, the 2.13-only module with quasiquotes and the Scala 3 module with quotes would have to depend on the same module (either 2.13 or 3) to avoid different runtime versions of the same type class
It already creates something like:
Scala 2.13 OR 3 <──── Scala 2.13 ONLY <──── Scala 3 ONLY
└ runtime code └ quasiquotes ├ quotes
├ def macro using quasiquotes
└ inline def using quotes
so even if we don't want a type class derivation defined in a companion object, it's a rather complex multimodule setup. If you want derivation (even the semi-automatic one), you have to introduce some non-obvious solutions that make even the simplest macro feel rather scary.
I have seen very few examples of macros that could use cross-building, and none of them implemented a type class derivation.
Meanwhile, Hearth has cross-quotes which are implemented by having something implemented differently on Scala 2 and on Scala 3, while the types are named the same way, so using Hearth is already violating the requirements of this scheme.
Changing Hearth to allow it would be possible, but it would introduce a huge burden on the maintainers without providing much value to (most) users.
It allows sandwich, however, with macros being expanded by Scala 2.13 only or Scala 3 only (2.13 module <- 3 module <- 2.13 module module or 3 module <- 2.13 module <- 3 module).
TASTy reader limitations
Scala 3's TASTy reader can consume some Scala 2.13 artifacts, but it has significant limitations that affect macro cross-compilation strategies. For a detailed analysis of what works and what doesn't, see The State of TASTy Reader and the Resources & Further Reading page.
What are the requirements to use this library?
Currently Hearth is built against:
- Scala 2.13.16
- Scala 3.3.8
with JDK 11 bytecode.
The features introduced by later versions of Scala/JDK (when already supported) are handled via runtime reflection and feature discovery.
These requirements might change, but we'll try to stay on the lowest version of Scala/JDK possible, while testing against the newest (currently 2.13.18, 3.8.4).
Can I use micro-FP/MIO in the production code?
It's not recommended.
It's not common knowledge, but macros are always expanded by the JVM - even if you are running them on a Scala.js or Scala Native build, because Scala.js/Native actually create both JVM bytecode as well as the code for the other platform.
At the same time, when Scala.js/Scala Native are linking the final code, they only do it for the code that was actually used and called, which means the macro code is stripped and it is not checked for JVM-only code.
This allows us to, e.g., implement DirectStyle using features that are unavailable on Scala.js/Scala Native and would not be valid code
for cross-compilation. But if these "illegal" calls only happen in a macro, which is stripped before linking by Scala.js/Scala Native,
the issue never occurs.
Hearth is intended to be used within macros.
So if you only use it within macros, it works for all versions of Scala: JVM, JS, and Native. If you use it in production code it might not link correctly on Scala.js/Scala Native.
Also, its MIO is not truly parallel - such functionality is not needed for macros.
How to fix forward reference to value ... defined on line ... extends over definition of value ...?
It happened when you write:
Depending on the platform, it translates to:
or
In both cases the native macro utility summons Type[A] - since we are just declaring implicit/given of that type
we are creating a circular dependency. We have to prevent the utility from summoning its own result by:
// In one scope, that does not define implicit Type[A]:
private val SomeType = Type.of[A]
locally {
// In another scope, where we can expose a computed result as an implicit:
implicit val someType: Type[A] = SomeType
}
While it's a bit mundane, it makes it rather explicit which types we are using or not, and where.
How to fix ScopeException: Cannot call 'asTerm' on an 'Expr' that was defined in a different 'Quotes' context?
This error occurs when native Scala 3 '{ ... } / ${ ... } syntax is used with Hearth's builders (e.g. LambdaBuilder,
ValDefBuilder) without properly managing the scala.quoted.Quotes context.
The full error message example
The call site error example
Most likely cause: you are using native Scala 3 quotes ('{ ... } / ${ ... }) in a Scala 3-only codebase without wrapping
them with passQuotes and withQuotes. Hearth builders store closures that may execute in a different Quotes context than the
one captured from the lexical scope, causing a mismatch.
Fix: wrap every ${ ... } splice that calls Hearth utilities with passQuotes, and every '{ ... } quote with withQuotes:
Wrapping with passQuotes and withQuotes
See passQuotes and withQuotes for a full explanation and runnable example.
Not an issue with Cross Quotes
If you use Expr.quote / Expr.splice (Cross Quotes), the Quotes context is managed automatically
and this error should not occur. If it does, please
file a bug report.