An experimental Scala 3 library for structural quasiquote research. The repository separates compiler-free representations and algorithms from compiler-coupled parsing, reflection, and lowering.
The project is a research proof of concept. Its API, coordinates, supported
syntax, and compatibility policy may change. The immutable 0.2.0 core and
matching 3.3.8/3.8.4 frontend artifacts are available from Maven Central;
the current 0.3.0-SNAPSHOT tree is not published. Its candidate release
topology expands to eleven artifacts under an explicit fail-closed release mode;
that local readiness is not Maven availability.
The canonical architecture has one project-owned, compiler-free semantic model with multiple source frontends. Current-Dotty is the released/default reference route; the Scalameta typed route is an explicit, unpublished experiment rather than a second quasiquote engine.
qr constructs a Scala 3 quoted-reflection Term from source-like syntax
with structural splices.
For an explicit receiver whose member name is computed during macro expansion,
SelectedMemberName.from(decoded) provides a validated compiler-free name
value that can occupy only the selection-name slot, for example
qr"$receiver.$selectedName($argument)". Ordinary String values are not
name holes, and this surface does not perform lexical or symbol lookup by
string.
import scala.quoted.*
import quasiquotes.construct.Quasiquotes.*
object ReadmeQuickStart:
inline def add(left: Int, right: Int): Int = ${ addImpl('left, 'right) }
private def addImpl(left: Expr[Int], right: Expr[Int])(using Quotes): Expr[Int] =
import quotes.reflect.*
addImplTerm(left.asTerm, right.asTerm).asExprOf[Int]
private def addImplTerm(using q: Quotes)(left: q.reflect.Term, right: q.reflect.Term): q.reflect.Term =
qr"$left + $right"The layers are explicit: the inline macro receives typed staged Expr[Int]
values, converts them to low-level quotes.reflect.Term trees, uses qr for
source-like structural construction at that reflection-tree layer, and converts
the resulting Term back to Expr[Int]. A quotes.reflect.Term is generally a
typed quoted-reflection tree in macro use; it is not the compiler-internal raw
dotty.tools.dotc.ast.untpd.Tree used only by the unpublished dottyInternal
module.
This exact example is compiled from an external-package fixture. See
Getting started for the larger construction,
matching, type, Lambda1, P1/P2 block, source-owned local-definition, and
compiler-free definition examples. See
execution environments and AST representations
for compile-time macros, runtime staging, compiler-free values, and
compiler-backed parsing without Quotes.
| Role | Interpolated syntax | Interpolator availability | Programmatic API | Function/API availability |
|---|---|---|---|---|
| Term construction | qr"..." |
Public now | QuasiquoteBuilder.build(...) |
Public now |
| Term pattern matching | case qq"..." |
Public now | QuasiPattern.term(...), termOrThrow(...) |
Public now |
| Type construction | tqr"..." |
Public now | QuasiTypequotes.tqr(...) |
Public research API |
| Type pattern matching | case tqq"..." |
Public now | QuasiTypequotes.tqq(...) / QuasiTypePattern.* |
Public research API |
| Definition construction | dqr"def id(x: $parameterType): $resultType = x" |
Public now, exact bounded shape | DefinitionConstruction.* |
Public bounded compiler-free API |
| Definition pattern matching | case dqq"def id(x: Int): Int = $body" |
Public now, exact bounded shape | DefinitionPattern.singleParameter(...) |
Public now, exact bounded shape |
qr is the ergonomic aborting term-construction syntax;
QuasiquoteBuilder.build is its recoverable programmatic counterpart. The
bounded qq extractor returns caller-owned quotes.reflect.Term captures in
left-to-right slot order. It admits term slots only, treats every slot as
distinct, returns ordinary mismatch through pattern fallthrough, and reports a
malformed template during macro expansion. Use QuasiPattern.term or
termOrThrow for explicit diagnostics and named/repeated-hole semantics.
qr also accepts a caller-owned quotes.reflect.TypeRepr as the complete
constructor Type of its bounded one-list new form and as each complete Type
in the bounded source-owned local-definition form. TypeRepr.of[T],
TypeTree.of[T].tpe, and a tqr result use the same transport, including
direct tqr"java.lang.StringBuilder" to qr"new $typeValue(arg)" stacking.
Passing Type[T] or TypeTree directly, partial/applied dynamic Type
positions, and broader constructor topology remain unsupported.
For runtime-length Term arguments, wrap an immutable sequence from the active
caller Quotes with TermSequenceSplices.termSplice and retain the explicit
rank marker in source: qr"f(..$args)" or
qr"new $constructorType(..$args)". This construction-only surface admits
one repeated hole in one ordinary Apply or supported one-list New argument
list. It preserves order and original Term subtrees; sequence matching, other
splice ranks, additional clauses, and target vararg-star semantics are absent.
The type names are intentionally layered overloads. Inside an active Quotes,
tqr"..." accepts caller-owned TypeRepr splices and returns a caller-owned
TypeRepr; case tqq"..." returns original target subtrees in source-slot
order. The same imports retain the recoverable neutral functions
QuasiTypequotes.tqr(...) and QuasiTypequotes.tqq(...). The interpolated
slots are distinct ordinal positions, while named and repeated-hole semantics
remain available through the programmatic API. DefinitionConstruction.* is
bounded compiler-free semantic construction/projection. The public dqr
interpolator is a separate caller-owned Quotes surface. Its original
single-parameter shape admits two supported TypeRepr splices and a literal
body naming that parameter; accepted current-Dotty semantics also admit one
bounded exact-two ordinary-parameter clause with three TypeRepr splices and
a literal body selecting either generated binder. The exact-two slice is
currently limited to standalone Int, String, and Boolean; it is not
N-parameter Definition parity. The unchanged variadic signature returns a
DefDef owned by the current Symbol.spliceOwner for immediate placement in
the same macro-generated local block. It is not a detached tree, body-hole API,
or general owner/placement facility.
Two additive umbrella façades are public in the current source snapshot:
import quasiquotes.Quasiquotes.{qr, qq, tqr, tqq, dqr, dqq}
import quasiquotes.scalameta.Quasiquotes.{qr, qq, tqr, tqq, dqr, dqq}They directly export the six established current-Dotty and typed-Scalameta families respectively; they do not duplicate parsing, matching, lowering, or reflection semantics. Every original domain-specific import above remains supported.
The public Definition-pattern direction uses the same spelling, dqq, as
template structure grows. A static exact-one template retains the precise
legacy SingleParameterDefinitionPattern result. A static structural
exact-two template specializes to the scalable DefinitionPatternExtractor,
while a dynamic/non-static dqq call retains the truthful historical
single-parameter fallback. DefinitionPatternExtractor represents structural
clauses and parameters rather than an arity-numbered public family: there is no
public dqq2, dqq3, or dqq4 API and no public
TwoParameterDefinitionPattern. Both admitted static forms capture the
caller's exact original RHS q.reflect.Term; ordinary mismatch falls through.
The separate DefinitionPattern.singleParameter(...) programmatic matcher
remains exact-one and additionally preserves the original reflected parameter
and result types. Typed-Scalameta dqq has the same accepted static
exact-one/exact-two selector split and the same dynamic exact-one fallback.
Its exact-two slice preserves the same bounded structure and caller-owned RHS
identity without routing through the neutral Definition projectors.
See the syntax support matrix for the user-facing construct/match boundary and the cross-surface capability matrix for the independent Q, typed-Scalameta, neutral, fresh-lowering, and existing-tree rewrite directions. The projection, lowering, and bridge pipeline matrix names the concrete APIs, visibility boundaries, and current composition status.
- Macro-Paradise for Scala 3 is an experimental Scala 3 pre-typer macro-annotation compiler plugin; Quasiquotes integration is optional research, not a core product dependency.
- AUXify-scala3 is an experimental Scala 3 AUXify reimplementation using Macro-Paradise; its narrow handler paths use Scalameta source-like authoring plus definition-specific Quasiquotes exact lowering bridges.
corecontains compiler-free term/type/definition values, construction, matching, source metadata, and stable diagnostic projections.frontendsupplies Scala 3 compiler-coupled parsing, macros, quoted reflection adapters, and public source-oriented conveniences.neutralScalametais a remotely unpublished compiler-free experiment backed by Scalameta 4.17.3. It provides direct source-AST authoring plus a bounded structural projection into the existing validated IR, including the accepted fully-qualified, non-generic, one-positional-list constructor/New family, withoutQuotes, compiler implementation dependencies, staging, SemanticDB, or exact trees. Its boundedScalametaTermShapeAuthoringreverse direction creates freshPosition.NoneScalameta Terms for binder-free ordinary terms, fully-qualifiednew, binder-free P1 blocks, and standard-sinterpolation with exact semantic round trip; P2/P3 binder authoring and source-provenance reconstruction remain outside.hybridScalametaFrontendis a remotely unpublished, compiler-coupled side-by-side experiment. It contains explicit typed Term, Type, and bounded Definition opt-in APIs inquasiquotes.scalameta. They parse public Scalameta ASTs, lower into existing project semantics, and retain current-Dotty as the reference/oracle. Only a Scalameta parse failure may use the current parser as fallback; semantic or lowering failures remain fail-closed. Publicqr/qqandtqr/tqqdefaults and published dependencies do not change. Its typeddqr/dqqreuse the current-Dotty one-ordinary-parameter and exact-two Definition lowerers and matchers for their overlapping slices; neutral Scalameta definition authoring and typed reflected Definition placement remain distinct contracts.dottyInternalcontains exact-compiler internal adapters, the public bounded exact-versionScalametaTermUntypedBridgeand context-freeScalametaTypeUntypedBridge, the public bounded exact-versionScalametaTermGeneratedOriginBridge, the bounded exact-version source-free and generated-origin Scalameta Definition bridges, the public bounded hybridScalametaDefinitionClassMemberAppendBridge, and five narrow experimental foreign-package peer bridges: contextual-method lowering, bounded AUXify self abstract-Type-member lowering, delegated forwarding, and the bounded three-parameter refined Type alias, plus the exact bounded instance-factory bridge. Its richer package-private Term backend also accepts the bounded one-local-val P2 block when authoritative completed Type sidecars are available and the bounded P3 local-identity-definition block when authoritative parameter/result completed-Type sidecars are available. The narrower direct Core lowerer remains intentionally closed to P2 and P3. Its source is present for review and testing, and its artifact remains remotely unpublished. It is nevertheless a normally publishable production project; consumers of any future coordinate must match the exact Scala compiler version.public-core-examplesandpublic-api-examplescompile consumer code from outside the library packages.
In role, frontend is closest to Scala 2 quasiquotes: it owns source-like
quotation/pattern syntax and compiler-coupled construction and matching.
core remains a small project-owned validated structural model rather than a
full Scala AST. The experimental Scalameta layer sits above it and projects
only admitted shapes downward; it does not make core depend on Scalameta.
See the neutral Scalameta experiment,
the hybrid typed frontend experiment,
the Scalameta opt-in artifact topology,
and the Dotty-internal exact backend. Its
public Term surfaces are the focused
bounded source-free Scalameta Term bridge
and separate
bounded generated-origin Scalameta Term bridge;
its
public Type sibling is the
bounded Scalameta Type bridge; its
public hybrid Definition/existing-class composition seam is the
bounded class-member append bridge;
its
foreign-package definition surfaces include the focused
contextual-method bridge
and self abstract-Type-member bridge.
Requirements are JDK 25 and sbt 1.12.15. The required baseline is Scala 3.8.4.
sbt -batch clean test publicCoreExamples/test publicApiExamples/test \
core/verifyCoreBoundary neutralScalameta/verifyNeutralScalametaBoundary \
verifyModuleGraph packageThe build serializes tasks and uses exported test/compile JARs with flat test class-loader layering to keep the aggregate gate deterministic.
The latest Maven Central release is the immutable version 0.2.0:
libraryDependencies +=
"com.github.dmytromitin" %% "quasiquotes-scala3-core" % "0.2.0"
libraryDependencies +=
"com.github.dmytromitin" %
"quasiquotes-scala3-frontend_3.8.4" % "0.2.0"core uses ordinary Scala 3 binary crossing. frontend uses full compiler
version crossing and must match the consuming compiler line. The released set
contains core_3 plus frontend artifacts for Scala 3.3.8 and 3.8.4 only. The
current source tree is the unpublished development version 0.3.0-SNAPSHOT
and is not interchangeable with these released coordinates.
All five production modules are normally publishable sbt projects. The
candidate 0.3.0 topology is exactly core_3, binary-crossed
neutral-scalameta_3, and full-crossed frontend, scalameta-frontend, and
dotty-internal for Scala 3.3.8, 3.8.4, and final 3.9.0: eleven coordinates
total. No special property is needed to package or stage them in a task-owned
local repository. All three compiler lines are required CI lanes, the
binary-cross artifacts are built once with 3.3.8, the root/examples remain
skipped, and no 0.3.0 coordinate is remotely released by this policy.
See Getting started, execution environments and AST representations, diagnostics, architecture, neutral Scalameta experiment, hybrid typed frontend experiment, syntax support matrix, projection, lowering, and bridge pipelines, exact constructor backend, supported syntax and limitations, why quasiquotes?, north-star quasiquote examples, typed class, symbol, and owner feasibility, compatibility, public API shape compatibility review, statement-ADT 0.2-to-0.3 compatibility qualification, versioning and stability, and the release process.
The machine-readable 0.2.0 public API baseline
contains 305 core and 313 frontend Scaladoc-visible entries. It excludes the
root, unpublished experimental neutralScalameta, unpublished
hybridScalametaFrontend, unpublished dottyInternal, and package-private
internals. It is generated from packaged Scaladoc search metadata for
deterministic source/API-shape diffing; it is neither human API documentation
nor binary, TASTy, overload-resolution, or semantic compatibility proof.
The controller-accepted current standard candidate inventory is 679 rows / 661
groups, while the unpublished typed-Scalameta inventory remains 43 rows / 43
groups. The typed exact-two selector replaces one source signature while
retaining its historical erased JVM descriptor through a source-hidden bridge. These
development counts do not alter the immutable 0.2.0 baseline or imply a
remote 0.3.0 release.
The structural type subset includes recursively nested List and Option
applications plus binary Either, including patterns, construction, quoted
lowering/inspection, typed ascriptions, and scoped type evidence. Constructor
admission remains fixed. An experimental programmatic
GlobalSelectedTypeFrontend accepts canonical globally addressable selected
names only through an explicit GlobalSelectedTypeEnvironment built from
typed witnesses. It supports selected standard List/Option/Either by
full declaration identity; it does not add general name resolution, aliases,
stable-term paths, or ambient lookup. Interpolated tqr additionally admits a
zero-hole canonical globally selected class terminal such as
java.lang.StringBuilder, resolved through an exact typed witness; this does
not admit aliases, stable-term paths, or selected constructor applications,
and tqq remains unchanged.
The canonical first-use examples, including the complete Lambda1, bounded P1
block and single-typed-local-val P2 qr/qq, and bounded tqr/tqq macro paths, are mirrored from compiled
external-package fixtures, and the repository's snippet drift check compares
them byte for byte.
Public type diagnostics describe the supported boundary without development
chronology or generated placeholder names.
The compiler-free public API constructs bounded single- and exact-two-parameter
methods whose bodies explicitly select a declared parameter. That public name
selection is converted once to the package-private binder-aware definition
core; a free same-text CompletedTerm.reference is never captured implicitly.
This is a semantic construction/projection API, not a source parser or
method-placement backend.
This project is licensed under the Apache License, Version 2.0. See LICENSE. Repository visibility and later artifact publication remain separate decisions.
Support expectations are intentionally conservative; see Support and Security policy. No private security-reporting channel is currently offered or promised for this experimental research stage. Do not post sensitive material publicly merely to obtain maintainer attention.
The experimental frontend also supports bounded fully-qualified constructor
expressions such as new java.lang.StringBuilder(16) for qr construction and
structural patterns. See the supported-syntax document for the deliberate
generic, imported-name, multiple-list, and anonymous-class exclusions.
One ordinary explicitly typed Lambda1 form is also available for structural
qr construction and matching. Its project-owned binder identity provides
alpha-aware bound-reference comparison and same-text splice non-capture. The
unpublished exact internal backend supports that same bounded Lambda1 shape in
source-free and generated-origin modes.
Binder-free P1 blocks are also available through ordinary qr construction
and qq/programmatic matching. They preserve one or more ordered expression
prefixes and a distinct final result. P2 separately admits one typed eager
immutable local val. Construction-only source-owned local-definition support
now admits exactly one literal method with one ordinary parameter, complete
parameter/result TypeRepr holes, a parameter-reference body, and one following
result; broader statements and qq local-definition matching remain excluded.
The unpublished neutral module has separate reusable internal projectors
for an explicitly typed immutable val, a true parameterless explicitly typed
def, one- and two-ordinary-parameter explicitly typed defs, and a simple
non-generic unbounded Type alias. One package-private dispatcher selects exactly
those five families. The unpublished exact-version
ScalametaDefinitionUntypedBridge now composes that dispatcher with the common
Definition-shape lowerer and returns a fresh source-free untpd.MemberDef for
all five families. The separate
ScalametaDefinitionGeneratedOriginBridge returns positioned generated-origin
members for only the four concrete val/def families; the simple alias is
deliberately rejected because no generic alias-origin authority is admitted.
Parameter references are resolved by project binder identity rather than
display text. Neither bridge generalizes parameter-list syntax nor owns target
admission, insertion, rollback, ordinary typing, or owner/reownership work. See
the bounded Definition bridge contract.
ScalametaDefinitionClassMemberAppendBridge is the separate bounded hybrid
composition for an already parsed pre-Typer ordinary class. It authors one of
those four generated-origin val/def families, delegates exact append-last
reconstruction to the existing-tree authority, and returns the rebuilt class
plus the exact appended member. Old members retain their original objects and
source, the new member retains its generated virtual source, and only the
enclosing class/Template shells are fresh at their original replacement site.
Macro/plugin lifecycle, target selection, rollback, and ordinary typing remain
caller-owned.