Skip to content

Latest commit

 

History

54 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Macro-Paradise for Scala 3

Macro-Paradise for Scala 3 is an experimental compiler-plugin project inspired by Scala 2 Macro Paradise. It explores pre-typer annotation expansion so that generated class members, companions, and sibling definitions are available to ordinary Scala typing in the same compilation run.

The core mechanism and a precompiled external-handler path are executable and well tested. The project is still compiler-sensitive research: its API, configuration, supported shapes, and compatibility policy may change. The immutable 0.1.1 release supports exact Scala 3.3.8, 3.8.4, and stable 3.9.0 and is available from Maven Central. Current main continues on the experimental 0.2.0-SNAPSHOT development line.

Quick start with the released template

The fastest verified released setup is the public macroparadise-scala3.g8 template:

sbt new DmytroMitin/macroparadise-scala3.g8
cd <generated-project>
sbt "core/run"

Expected output:

Hello, Greeter!

The generated build requires JDK feature 25 and sbt 1.12.15. It defaults to exact Scala 3.9.0, with exact 3.3.8 and 3.8.4 also selectable, and resolves the immutable Macro-Paradise 0.1.1 release. Its macro-annotations, macro-handlers, and core projects demonstrate a user-owned marker, precompiled external handler, and ordinary consumer. Macro-Paradise remains experimental and exact-compiler-specific.

A small user-authored example

A user can define an annotation marker, its precompiled handler, and an annotated consumer. The marker carries only the handler identity:

package com.example.`macro`.annotations

import paradise3.api.expander
import scala.annotation.StaticAnnotation

@expander("com.example.macro.handlers.IdentityHandler")
final class identity extends StaticAnnotation

The first handler is intentionally identity/pass-through. It reads a plugin-minted ExpansionInput; user code does not construct or copy that value:

package com.example.`macro`.handlers

import dotty.tools.dotc.core.Contexts.Context
import paradise3.api.*

final class IdentityHandler extends ExpansionHandler:
  override def annotationName: String =
    "com.example.macro.annotations.identity"

  override def expand(input: ExpansionInput)(using Context): ExpansionOutcome =
    ExpansionEdit.finish(ExpansionEdit.start(input))

The consumer imports the marker and uses short @identity as the normal form:

package com.example.core

import com.example.`macro`.annotations.identity

@identity
class Something

This behavior-free first use proves discovery, binding, loading, invocation, and unchanged pass-through. The complete marker/handler/consumer tutorial and both manual sbt translations are in External handler authoring.

The next step is source-like member generation. The canonical example uses Scalameta q syntax, Quasiquotes 0.3.0 generated-origin lowering, and the generic MacroParadise placement helper. The bridge supplies honest source/span provenance; no raw compiler constructor appears in this normal path:

package starter.handler

import dotty.tools.dotc.core.Contexts.Context
import paradise3.api.*
import paradise3.api.helpers.ExpansionHelpers
import quasiquotes.definitions.dotty.ScalametaDefinitionGeneratedOriginBridge
import scala.meta.*
import scala.meta.dialects.Scala3

final class GenerateGreetingHandler extends ExpansionHandler:
  val annotationName: String = "starter.marker.generateGreeting"

  def expand(input: ExpansionInput)(using Context): ExpansionOutcome =
    ExpansionEdit.finish:
      for
        edit <- ExpansionEdit.start(input)
        _ <- input.primary match
          case ExpansionTarget.Class(_) => Right(())
          case _ => Left(ExpansionDiagnostic("@generateGreeting requires a class primary", input.currentAnnotation.sourcePos))
        definition = q"""def generatedGreeting: String = "Hello, Greeter!" """.asInstanceOf[Defn.Def]
        lowered <- ScalametaDefinitionGeneratedOriginBridge
          .lower(
            definition,
            "<macroparadise-generated:GenerateGreetingHandler:generatedGreeting>"
          )
          .left
          .map(error => ExpansionDiagnostic(s"${error.code}: ${error.detail}", input.currentAnnotation.sourcePos))
        result <- ExpansionHelpers.placeMemberInPrimary(edit, lowered.tree)
      yield result

The handler project declares ("com.github.dmytromitin" % "quasiquotes-scala3-dotty-internal" % "0.3.0").cross(CrossVersion.full). Its marker points to GenerateGreetingHandler; after both are compiled, an ordinary consumer can use new Greeter().generatedGreeting. The repository mechanically compiles and runs this exact source in the independent external handler starter. Direct untpd construction remains an expert escape hatch, not the primary authoring path.

This fixture is intentionally narrow. It is evidence for the compiler mechanism, not a general-purpose macro-annotation API.

Exact toolchain

The source build requires:

  • JDK feature version 25;
  • sbt 1.12.15;
  • Scala 3.3.8, 3.8.4, or stable 3.9.0, selected as a separate exact build lane.

The build rejects other JDK feature versions before normal tasks run. The plugin and handler contract expose Scala compiler internals, so a nearby Scala version is not an interchangeable substitute.

The JDK contract is feature version 25, not one vendor or 25.0.x patch. The root .java-version contains 25 as a convenient local version-manager hint; the build-time verifier remains authoritative. See Getting started for concise jenv and SDKMAN setup guidance.

IntelliJ IDEA import

IntelliJ must launch sbt itself on JDK 25; setting only the source language level or Scala SDK is insufficient. For this project, open Settings | Build, Execution, Deployment | Build Tools | sbt, set JVM | JRE to a JDK 25 installation, and also set the project SDK to JDK 25 under File | Project Structure | Project.

If an import command starts with a Java 8 executable such as .../corretto-1.8.../bin/java, IntelliJ has selected the wrong sbt JVM. Changing the Scala version is not the fix. Select JDK 25 in both locations, then refresh or reimport the sbt project. An unsupported importer JVM is rejected during meta-build settings load, before the ordinary project/*.scala helpers compile.

For packaged external-handler projects, delegate build and run actions to sbt. The sbt-imported workflow is qualified; IntelliJ's native JPS compiler path is not currently part of the supported boundary. The .java-version hint does not automatically configure IntelliJ's project SDK or sbt JVM.

Run the complete product gate from the repository root:

sbt -batch verifyPublicProductBoundary

The gate runs nonempty plugin and consumer suites, packages the plugin and experimental handler contract, checks the normalized API surface, exercises independent consumers, verifies the external-handler starter, and confirms that only the compiler plugin and handler API are top-level product artifacts for publishLocal. The separate source-built sbt module also validates its local/test packaging while every remote publication path remains fail closed.

For an additional product-only isolation proof, run the same canonical gate in a disposable source copy with fresh dependency and build caches:

scripts/verify-public-product-fresh-copy.sh

This companion gate copies only tracked and task-owned untracked product files; it does not read a controller checkout or reuse repository build output.

For a smaller ordinary development pass:

sbt -batch test

Release and development installation

The immutable Central release and current source checkout are separate states. Released 0.1.1 is available for each supported exact Scala line:

ThisBuild / scalaVersion := "3.3.8" // or exact 3.8.4 / 3.9.0
addCompilerPlugin(("com.github.dmytromitin" % "macroparadise-scala3-plugin" % "0.1.1").cross(CrossVersion.full))

To install current 0.2.0-SNAPSHOT from this checkout for one exact line, select that line explicitly and publish only to the machine-local repository:

sbt -Dmacroparadise.exactScalaVersion=3.3.8 -batch "++3.3.8!" "pluginApi/publishLocal" "plugin/publishLocal"
sbt -Dmacroparadise.exactScalaVersion=3.8.4 -batch "++3.8.4!" "pluginApi/publishLocal" "plugin/publishLocal"
sbt -Dmacroparadise.exactScalaVersion=3.9.0 -batch "++3.9.0!" "pluginApi/publishLocal" "plugin/publishLocal"

Then a local development consumer uses the matching exact line and snapshot:

ThisBuild / scalaVersion := "3.3.8" // or exact 3.8.4 / 3.9.0
addCompilerPlugin(("com.github.dmytromitin" % "macroparadise-scala3-plugin" % "0.2.0-SNAPSHOT").cross(CrossVersion.full))

CrossVersion.full is required; %% produces only a binary Scala suffix and does not name this exact-compiler plugin. Release 0.1.1 is published for all three exact lines. The 0.2.0-SNAPSHOT coordinate is source-build/local- publication support only and is not published remotely.

The plugin JAR is self-contained for compiler loading: it embeds the exact unshaded paradise3.api runtime classes that the plugin links against. Its POM does not add a second runtime API dependency. An ordinary API dependency is a source-compilation dependency for marker/handler authors; it is not, and need not be, part of -Xplugin.

Authors of precompiled annotation markers and handlers also use the exact full-cross macroparadise-scala3-plugin-api coordinate described in External handler authoring. Ordinary plugin-only users do not add implementation or repository test artifacts.

The supported external-handler flow has three precompiled stages: marker, handler, then annotated consumer. Choose one of two top-level setups:

  • sbt integration (recommended normal path): use same-build local marker and handler projects with no producer publishLocal, or use genuinely published marker/handler modules. Marker-only local projects normally use provided->compile, and marker-only published modules normally use % Provided, keeping marker API available for compilation but absent at runtime;
  • fully manual: keep the ordinary marker dependency and wire the compiler plugin, complete handler expansion classpath, and content identity directly.

The sbt-integration module documents both plugin producer modes. Its 0.1.1 release is available remotely for normal use:

addSbtPlugin("com.github.dmytromitin" % "sbt-macroparadise" % "0.1.1")

To test current 0.2.0-SNAPSHOT source instead, first run sbt -batch publishLocal inside sbt-integration/ and select that local snapshot in project/plugins.sbt. The static local-project helper deliberately does not infer the marker dependency; the consumer must still declare .dependsOn(macroAnnotations % "provided->compile") for marker-only API, or a plain dependency for intentionally runtime-bearing marker API. Current source also provides a Seq[ProjectReference] overload for multiple local marker and handler projects. The complete manual escape hatch is in External handler authoring.

The plugin loader sees the self-contained plugin JAR; the ordinary source classpath sees the API and precompiled marker; the handler is selected through -P:macroparadise:handlerClasspath=<handler-jar>. A second build-only option contains a SHA-256 identity derived from every explicit marker artifact and the complete ordered effective handler expansion classpath, including handler dependencies, so Zinc invalidates consumers when any role input changes at a stable path. The running application does not need the compiler plugin or handler JAR merely because they were used at compile time. Same-compilation marker metadata is not currently claimed.

External-handler starter

Start with the minimal user-defined @identity example. Its handler returns the annotated class unchanged, so successful compilation isolates marker discovery, metadata binding, imported-short canonicalization, handler loading, and one invocation from generated-member behavior. The independent external sbt proof also compiles the direct-qualified control and checks the exact marker/handler/ consumer dependency graph.

sbt -batch verifyIndependentExternalSbtConsumerFromLocalRepository

Then use the fixture-independent generateGreeting starter to prove generated- member typing and runtime behavior on the same three-role topology. Its packaged precheck retains the maximum-witness explicit form and a bounded compact form without changing the experimental handler API.

sbt -batch verifyExternalHandlerAuthoringStarter

The copy/paste identity tutorial, including a no-AutoPlugin published-module recipe, is in External handler authoring; the executable starter example is the generated- output follow-on. The exact hyphenated-directory fixture for same-build manual, same-build local-project, AutoPlugin published-module, and manual published-module setup is retained under examples/user-onboarding-three-mode-fixture.

Supported experimental boundary

The current implementation provides bounded evidence for:

  • top-level class rewriting before typer;
  • generated class methods;
  • companion creation and existing-companion merge;
  • generated sibling classes;
  • structured primary, companion, and ordered additional-output roles;
  • a released experimental syntactic pre-typer read-only view of ordered direct members and bounded direct-method structure, with a deliberately tiny type-shape normalization;
  • raw untyped output as an expert escape hatch;
  • precompiled external handlers selected explicitly or by marker metadata;
  • qualified syntactic annotation identities and unambiguous package-level explicit-import canonicalization;
  • plugin-owned, source-ordered composition with applicability decided inside each handler's expand method;
  • a restricted generic-trait contextual companion-method fixture.

Legacy source-annotation composition is fail closed. Every participant must explicitly accept the concrete target inside expand, preserve remaining handled annotations exactly, and satisfy the plugin's output and rollback invariants. The coordinator is generic, but positive evidence remains bounded to the combinations in the test suite.

Important limitations

  • External handlers and their raw tree values are tied to the exact compiler build.
  • Annotation matching is syntactic. One unambiguous, source-preceding, package-level explicit import is supported; alias, wildcard, local/nested, given, export, symbol, and general semantic resolution are not implemented.
  • General same-module handler support is deferred. Current main contains a separate opt-in experimental implementation of one bounded different-file Model A: one explicit marker source, one explicit handler source, exact source-byte identity, compiler-unit suspension, and fresh current-output handler loading. Exact Scala 3.3.8, 3.8.4, and 3.9.0 CLI/Zinc qualification passes. Persistent sbt BSP and live IntelliJ qualification remain bounded to exact Scala 3.3.8 and 3.8.4 when the sbt-imported project delegates Build and Run to sbt on JDK 25 and sbt 1.12.15, including handler-only edits without clean and a fresh session after close/reopen. Same-file marker/handler/consumer topologies, dependency cycles, automatic discovery, multiple configured relationships, and native IntelliJ/JPS compilation remain rejected, unimplemented, or unqualified, so general same-module support remains false. Precompiled handlers remain the broad/default supported experimental path.
  • The project does not provide arbitrary target shapes, arbitrary definition construction, arbitrary composition, typed tree construction, semantic member analysis, or a stable public API. The bounded body and type-structure views perform no typing, symbol/owner lookup, inheritance, alias expansion, subtyping, or overload resolution. The additive type-structure view makes absent, present-supported, and present-unsupported source bounds distinct, and separates abstract bounded direct type members from aliases, polymorphic, modifier-bearing, and unsupported forms. Its shared simple named-type case records only an unqualified syntactic name such as String or Nat; applied, qualified, refined, function, and broader shapes remain unsupported. Raw ExpansionInput.primary.tree remains the exact-compiler escape hatch.
  • Quasiquotes integration is optional cross-project research, not a product build dependency.
  • Top-level local publication is enabled only for the exact-cross plugin and handler API; the sbt module has separate packaging and a published 0.1.1 coordinate. Released 0.1.1 includes the bounded direct-body and type- structure views but has no implied release cadence or production support commitment.
  • Released 0.1.1 has no public object-target routing. Current 0.2.0-SNAPSHOT supports one ordinary top-level object through the role-aware handler API, with immutable helper composition inside one handler. General public U-style existing-definition transformation authoring is not a released API.

See Supported scope and limitations for the detailed boundary.

Related projects

  • Quasiquotes for Scala 3 explores a compiler-neutral Scalameta authoring model and exact-compiler lowering adapters. It is independent, optional research rather than a Macro-Paradise product dependency.
  • AUXify for Scala 3 is an independent downstream consumer. Its current source-built development slices include bounded @apply, @aux, @instance, @delegated, and default @self handlers; @syntax is characterized but not implemented. See the real downstream examples for exact status and ownership boundaries.

Documentation

Contributing, support, and security

Before proposing a change, read Contributing. For usage questions and defect reports, see Support. Security-sensitive material must not be posted publicly; the current reporting limitation is explained in the Security policy.

License and publication status

The source is licensed under the Apache License 2.0. The plugin and handler-facing API remain experimental, compiler-version-specific, and without stability guarantees. The plugin, plugin API, and sbt integration are published as 0.1.1; compiler-facing artifacts use exact full crossing for Scala 3.3.8, 3.8.4, and 3.9.0. Current 0.2.0-SNAPSHOT development is not published remotely. Internal fixtures, tests, examples, consumers, and spikes remain unpublished.

See Expansion model and composition for mandatory primary roles, optional opposites, lease omission, and atomic helper programs.

About

Experimental Scala 3 Macro Paradise: pre-typer macro annotations that can generate class members, companions, and sibling definitions visible to ordinary typing. Extends Scala 3 macro-annotation experiments with precompiled external handlers, source-ordered composition, validation, and rollback.

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages