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 0.3.0 release is available
from Maven Central for Scala 3.3.8, 3.8.4, and 3.9.0. Its eleven published
coordinates cover Core, frontend, neutral Scalameta, Dotty-internal, and typed
Scalameta frontend roles. The annotated v0.3.0 tag and matching GitHub
Release identify the published source.
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, released opt-in experiment rather than a second quasiquote engine.
Choose the representation by the contract you need: Scalameta for broad source
AST work, the project-owned compiler-free model for bounded normalized Term,
Type, and Definition semantics, and exact-version Dotty untpd for fresh raw
lowering or identity-sensitive existing-tree transformation. The canonical
semantic models and conversions guide
defines Q/N/U-D/U-U/C, shows the current conversion graph, and contains
compile-checked public Term, Type, SemanticDefinition, Scalameta conversion,
semantic-lowering, and exact-bridge hello worlds. The public five-family
Definition Projection/Authoring pair and the Term/Type/Definition source-free
lowering facades are current; exact-U transformation APIs remain internal or
planned and are labeled accordingly.
Generate a workspace with six small runnable examples:
sbt new DmytroMitin/quasiquotes-scala3.g8The starter repository
contains examples for Core, Q/frontend, N/neutral Scalameta,
U/Dotty-internal, Bridge/composition, and Hybrid typed Scalameta. The Bridge
example starts from a public source representation and composes semantic
projection/completion with exact lowering behind one public facade. It is a
cross-layer example, not public c* syntax or a sixth AST world.
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 exact-version 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 released 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 the accepted ordinary, fully-qualifiednew, standard-s, primitive-ascription, typed-Lambda1, binder-free P1, one-local-val P2, and local-identity-method P3 families with exact semantic round trip. Grouping parentheses remain source-origin syntax rather than a distinct project Term; source-provenance reconstruction remains outside.hybridScalametaFrontendis a released 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 its0.3.0artifacts are published. Consumers must match the coordinate to 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 0.3.0:
val quasiquotesVersion = "0.3.0"
libraryDependencies ++= Seq(
"com.github.dmytromitin" %% "quasiquotes-scala3-core" % quasiquotesVersion,
"com.github.dmytromitin" %% "quasiquotes-scala3-neutral-scalameta" % quasiquotesVersion,
("com.github.dmytromitin" % "quasiquotes-scala3-frontend" % quasiquotesVersion)
.cross(CrossVersion.full),
("com.github.dmytromitin" % "quasiquotes-scala3-dotty-internal" % quasiquotesVersion)
.cross(CrossVersion.full),
("com.github.dmytromitin" % "quasiquotes-scala3-scalameta-frontend" % quasiquotesVersion)
.cross(CrossVersion.full)
)core and neutral-scalameta use ordinary Scala 3 binary crossing.
frontend, dotty-internal, and scalameta-frontend use full compiler
version crossing and must match the consuming compiler line.
The published 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.
See Getting started, execution environments and AST representations, diagnostics, architecture, neutral Scalameta experiment, hybrid typed frontend experiment, syntax support matrix, semantic models and conversions, 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, the modules that were not part of 0.2.0 (neutralScalameta,
hybridScalametaFrontend, and 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 0.3.0 standard inventory is 794 rows / 775 groups,
including the additive binder-safe Term and semantic-Definition APIs and the
construction-only runtime-sequence tqr overload,
while the 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
0.3.0 counts do not alter the immutable 0.2.0 baseline.
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 standard typed frontend also constructs tqr"$constructor[..$arguments]"
from one caller-owned class-constructor TypeRepr and one runtime-length
ordered Seq[TypeRepr]. This construction-only overload validates arity, kinds
and supported reflected forms directly in the caller's Quotes universe; it
preserves constructor and argument identities without normalizing through
TypeNormalForm. Scalar tqr and tqq retain their existing behavior. General
TypeLambda authoring, aliases-as-aliases, instance-dependent prefixes,
refinements and nontrivial constrained bounds remain outside this slice;
typed-Scalameta runtime-sequence construction is not implemented.
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 includes the older bounded Definition construction
surface and the accepted SemanticDefinition model. SemanticDefinition
constructs bounded immutable values, concrete methods with zero/one/two
ordinary parameters, and simple aliases through validated names, clauses,
types, bodies, and typed views. Method bodies select parameters through the
persistent opaque DefinitionParameterScope; a free same-text identifier is
never captured implicitly. Public generic Scalameta Definition
Projection/Authoring now converts the current five-family intersection to and
from fresh Scalameta Defn values. Public Term, Type, and Definition semantic
lowerers also produce fresh source-free exact-version untpd trees. These
APIs are not source-token inverses, placement services, owner assignment, or
typechecking backends.
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
released 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 released neutral module exposes bounded public
ScalametaDefinitionProjection and ScalametaDefinitionAuthoring facades 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. Their private carrier and family dispatchers
remain implementation details. The released exact-version
ScalametaDefinitionUntypedBridge remains a separate non-delegating
composition for the same source-free five-family result.
DefinitionGeneratedOriginLowering takes
public SemanticDefinition plus a virtual source name and returns a fresh
positioned member with deterministic generated source. It includes simple type
aliases. Its semantic-model guide
describes failures, freshness, and the caller's placement responsibilities.
ScalametaDefinitionGeneratedOriginBridge returns positioned generated-origin
members for only the four concrete val/def families; the simple alias is
deliberately rejected at the bridge's historical compatibility boundary.
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.
The additive semantic Term generated-origin facade
accepts public TermShape and a virtual source name, returning a positioned tree,
deterministic text and fresh SourceFile. This implemented public facade has a
smaller source-name/grouping bound than the source-free TermUntypedLowering. Existing Scalameta
bridges remain separate. No Type generated-origin sibling or public
existing-owner transaction is included.