Skip to content

Repository files navigation

quasiquotes-scala3

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.

Quick start

Runnable starter

Generate a workspace with six small runnable examples:

sbt new DmytroMitin/quasiquotes-scala3.g8

The 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.

Quasiquote surfaces

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.

Related projects

  • 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.

Modules

  • core contains compiler-free term/type/definition values, construction, matching, source metadata, and stable diagnostic projections.
  • frontend supplies Scala 3 compiler-coupled parsing, macros, quoted reflection adapters, and public source-oriented conveniences.
  • neutralScalameta is 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, without Quotes, compiler implementation dependencies, staging, SemanticDB, or exact trees. Its bounded ScalametaTermShapeAuthoring reverse direction creates fresh Position.None Scalameta Terms for the accepted ordinary, fully-qualified new, 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.
  • hybridScalametaFrontend is a released compiler-coupled side-by-side experiment. It contains explicit typed Term, Type, and bounded Definition opt-in APIs in quasiquotes.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. Public qr/qq and tqr/tqq defaults and published dependencies do not change. Its typed dqr/dqq reuse 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.
  • dottyInternal contains exact-compiler internal adapters, the public bounded exact-version ScalametaTermUntypedBridge and context-free ScalametaTypeUntypedBridge, the public bounded exact-version ScalametaTermGeneratedOriginBridge, the bounded exact-version source-free and generated-origin Scalameta Definition bridges, the public bounded hybrid ScalametaDefinitionClassMemberAppendBridge, 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 0.3.0 artifacts are published. Consumers must match the coordinate to the exact Scala compiler version.
  • public-core-examples and public-api-examples compile 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.

Try the source build

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 package

The build serializes tasks and uses exported test/compile JARs with flat test class-loader layering to keep the aggregate gate deterministic.

Latest released coordinates

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.

License

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.

About

Experimental Scala 3 quasiquotes: Scala-2-style structural construction/matching for quoted terms, plus compiler-free term/type/definition models inspired by Scalameta. Incrementally exploring the missing low-level quasiquote layer for Scala 3.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages