Skip to content

Commit 4e4c151

Browse files
committed
Merge pull request scala#3235 from xeno-by/topic/macro-plugin-interface
new hooks in AnalyzerPlugins to enable macro experimentation
2 parents ada8d91 + 8791366 commit 4e4c151

40 files changed

+789
-228
lines changed

src/compiler/scala/reflect/macros/runtime/MacroRuntimes.scala

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,14 @@ trait MacroRuntimes extends JavaReflectionRuntimes with ScalaReflectionRuntimes
1919
* @return Requested runtime if macro implementation can be loaded successfully from either of the mirrors,
2020
* `null` otherwise.
2121
*/
22+
def macroRuntime(expandee: Tree): MacroRuntime = pluginsMacroRuntime(expandee)
23+
24+
/** Default implementation of `macroRuntime`.
25+
* Can be overridden by analyzer plugins (see AnalyzerPlugins.pluginsMacroRuntime for more details)
26+
*/
2227
private val macroRuntimesCache = perRunCaches.newWeakMap[Symbol, MacroRuntime]
23-
def macroRuntime(macroDef: Symbol): MacroRuntime = {
28+
def standardMacroRuntime(expandee: Tree): MacroRuntime = {
29+
val macroDef = expandee.symbol
2430
macroLogVerbose(s"looking for macro implementation: $macroDef")
2531
if (fastTrack contains macroDef) {
2632
macroLogVerbose("macro expansion is serviced by a fast track")

src/compiler/scala/tools/nsc/typechecker/AnalyzerPlugins.scala

Lines changed: 246 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ package typechecker
1313
trait AnalyzerPlugins { self: Analyzer =>
1414
import global._
1515

16-
1716
trait AnalyzerPlugin {
1817
/**
1918
* Selectively activate this analyzer plugin, e.g. according to the compiler phase.
@@ -156,6 +155,117 @@ trait AnalyzerPlugins { self: Analyzer =>
156155
def pluginsTypedReturn(tpe: Type, typer: Typer, tree: Return, pt: Type): Type = tpe
157156
}
158157

158+
/**
159+
* @define nonCumulativeReturnValueDoc Returns `None` if the plugin doesn't want to customize the default behavior
160+
* or something else if the plugin knows better that the implementation provided in scala-compiler.jar.
161+
* If multiple plugins return a non-empty result, it's going to be a compilation error.
162+
*/
163+
trait MacroPlugin {
164+
/**
165+
* Selectively activate this analyzer plugin, e.g. according to the compiler phase.
166+
*
167+
* Note that the current phase can differ from the global compiler phase (look for `enteringPhase`
168+
* invocations in the compiler). For instance, lazy types created by the UnPickler are completed
169+
* at the phase in which their symbol is created. Observations show that this can even be the
170+
* parser phase. Since symbol completion can trigger subtyping, typing etc, your plugin might
171+
* need to be active also in phases other than namer and typer.
172+
*
173+
* Typically, this method can be implemented as
174+
*
175+
* global.phase.id < global.currentRun.picklerPhase.id
176+
*/
177+
def isActive(): Boolean = true
178+
179+
/**
180+
* Typechecks the right-hand side of a macro definition (which typically features
181+
* a mere reference to a macro implementation).
182+
*
183+
* Default implementation provided in `self.standardTypedMacroBody` makes sure that the rhs
184+
* resolves to a reference to a method in either a static object or a macro bundle,
185+
* verifies that the referred method is compatible with the macro def and upon success
186+
* attaches a macro impl binding to the macro def's symbol.
187+
*
188+
* $nonCumulativeReturnValueDoc.
189+
*/
190+
def pluginsTypedMacroBody(typer: Typer, ddef: DefDef): Option[Tree] = None
191+
192+
/**
193+
* Expands an application of a def macro (i.e. of a symbol that has the MACRO flag set),
194+
* possibly using the current typer mode and the provided prototype.
195+
*
196+
* Default implementation provided in `self.standardMacroExpand` figures out whether the `expandee`
197+
* needs to be expanded right away or its expansion has to be delayed until all undetermined
198+
* parameters are inferred, then loads the macro implementation using `self.pluginsMacroRuntime`,
199+
* prepares the invocation arguments for the macro implementation using `self.pluginsMacroArgs`,
200+
* and finally calls into the macro implementation. After the call returns, it typechecks
201+
* the expansion and performs some bookkeeping.
202+
*
203+
* This method is typically implemented if your plugin requires significant changes to the macro engine.
204+
* If you only need to customize the macro context, consider implementing `pluginsMacroArgs`.
205+
* If you only need to customize how macro implementation are invoked, consider going for `pluginsMacroRuntime`.
206+
*
207+
* $nonCumulativeReturnValueDoc.
208+
*/
209+
def pluginsMacroExpand(typer: Typer, expandee: Tree, mode: Mode, pt: Type): Option[Tree] = None
210+
211+
/**
212+
* Computes the arguments that need to be passed to the macro impl corresponding to a particular expandee.
213+
*
214+
* Default implementation provided in `self.standardMacroArgs` instantiates a `scala.reflect.macros.contexts.Context`,
215+
* gathers type and value arguments of the macro application and throws them together into `MacroArgs`.
216+
*
217+
* $nonCumulativeReturnValueDoc.
218+
*/
219+
def pluginsMacroArgs(typer: Typer, expandee: Tree): Option[MacroArgs] = None
220+
221+
/**
222+
* Summons a function that encapsulates macro implementation invocations for a particular expandee.
223+
*
224+
* Default implementation provided in `self.standardMacroRuntime` returns a function that
225+
* loads the macro implementation binding from the macro definition symbol,
226+
* then uses either Java or Scala reflection to acquire the method that corresponds to the impl,
227+
* and then reflectively calls into that method.
228+
*
229+
* $nonCumulativeReturnValueDoc.
230+
*/
231+
def pluginsMacroRuntime(expandee: Tree): Option[MacroRuntime] = None
232+
233+
/**
234+
* Creates a symbol for the given tree in lexical context encapsulated by the given namer.
235+
*
236+
* Default implementation provided in `namer.standardEnterSym` handles MemberDef's and Imports,
237+
* doing nothing for other trees (DocDef's are seen through and rewrapped). Typical implementation
238+
* of `enterSym` for a particular tree flavor creates a corresponding symbol, assigns it to the tree,
239+
* enters the symbol into scope and then might even perform some code generation.
240+
*
241+
* $nonCumulativeReturnValueDoc.
242+
*/
243+
def pluginsEnterSym(namer: Namer, tree: Tree): Boolean = false
244+
245+
/**
246+
* Makes sure that for the given class definition, there exists a companion object definition.
247+
*
248+
* Default implementation provided in `namer.standardEnsureCompanionObject` looks up a companion symbol for the class definition
249+
* and then checks whether the resulting symbol exists or not. If it exists, then nothing else is done.
250+
* If not, a synthetic object definition is created using the provided factory, which is then entered into namer's scope.
251+
*
252+
* $nonCumulativeReturnValueDoc.
253+
*/
254+
def pluginsEnsureCompanionObject(namer: Namer, cdef: ClassDef, creator: ClassDef => Tree = companionModuleDef(_)): Option[Symbol] = None
255+
256+
/**
257+
* Prepares a list of statements for being typechecked by performing domain-specific type-agnostic code synthesis.
258+
*
259+
* Trees passed into this method are going to be named, but not typed.
260+
* In particular, you can rely on the compiler having called `enterSym` on every stat prior to passing calling this method.
261+
*
262+
* Default implementation does nothing. Current approaches to code syntheses (generation of underlying fields
263+
* for getters/setters, creation of companion objects for case classes, etc) are too disparate and ad-hoc
264+
* to be treated uniformly, so I'm leaving this for future work.
265+
*/
266+
def pluginsEnterStats(typer: Typer, stats: List[Tree]): List[Tree] = stats
267+
}
268+
159269

160270

161271
/** A list of registered analyzer plugins */
@@ -167,59 +277,158 @@ trait AnalyzerPlugins { self: Analyzer =>
167277
analyzerPlugins = plugin :: analyzerPlugins
168278
}
169279

280+
private abstract class CumulativeOp[T] {
281+
def default: T
282+
def accumulate: (T, AnalyzerPlugin) => T
283+
}
284+
285+
private def invoke[T](op: CumulativeOp[T]): T = {
286+
if (analyzerPlugins.isEmpty) op.default
287+
else analyzerPlugins.foldLeft(op.default)((current, plugin) =>
288+
if (!plugin.isActive()) current else op.accumulate(current, plugin))
289+
}
170290

171291
/** @see AnalyzerPlugin.pluginsPt */
172292
def pluginsPt(pt: Type, typer: Typer, tree: Tree, mode: Mode): Type =
293+
// performance opt
173294
if (analyzerPlugins.isEmpty) pt
174-
else analyzerPlugins.foldLeft(pt)((pt, plugin) =>
175-
if (!plugin.isActive()) pt else plugin.pluginsPt(pt, typer, tree, mode))
295+
else invoke(new CumulativeOp[Type] {
296+
def default = pt
297+
def accumulate = (pt, p) => p.pluginsPt(pt, typer, tree, mode)
298+
})
176299

177300
/** @see AnalyzerPlugin.pluginsTyped */
178-
def pluginsTyped(tpe: Type, typer: Typer, tree: Tree, mode: Mode, pt: Type): Type = {
179-
// support deprecated methods in annotation checkers
180-
val annotCheckersTpe = addAnnotations(tree, tpe)
181-
if (analyzerPlugins.isEmpty) annotCheckersTpe
182-
else analyzerPlugins.foldLeft(annotCheckersTpe)((tpe, plugin) =>
183-
if (!plugin.isActive()) tpe else plugin.pluginsTyped(tpe, typer, tree, mode, pt))
184-
}
301+
def pluginsTyped(tpe: Type, typer: Typer, tree: Tree, mode: Mode, pt: Type): Type =
302+
// performance opt
303+
if (analyzerPlugins.isEmpty) addAnnotations(tree, tpe)
304+
else invoke(new CumulativeOp[Type] {
305+
// support deprecated methods in annotation checkers
306+
def default = addAnnotations(tree, tpe)
307+
def accumulate = (tpe, p) => p.pluginsTyped(tpe, typer, tree, mode, pt)
308+
})
185309

186310
/** @see AnalyzerPlugin.pluginsTypeSig */
187-
def pluginsTypeSig(tpe: Type, typer: Typer, defTree: Tree, pt: Type): Type =
188-
if (analyzerPlugins.isEmpty) tpe
189-
else analyzerPlugins.foldLeft(tpe)((tpe, plugin) =>
190-
if (!plugin.isActive()) tpe else plugin.pluginsTypeSig(tpe, typer, defTree, pt))
311+
def pluginsTypeSig(tpe: Type, typer: Typer, defTree: Tree, pt: Type): Type = invoke(new CumulativeOp[Type] {
312+
def default = tpe
313+
def accumulate = (tpe, p) => p.pluginsTypeSig(tpe, typer, defTree, pt)
314+
})
191315

192316
/** @see AnalyzerPlugin.pluginsTypeSigAccessor */
193-
def pluginsTypeSigAccessor(tpe: Type, typer: Typer, tree: ValDef, sym: Symbol): Type =
194-
if (analyzerPlugins.isEmpty) tpe
195-
else analyzerPlugins.foldLeft(tpe)((tpe, plugin) =>
196-
if (!plugin.isActive()) tpe else plugin.pluginsTypeSigAccessor(tpe, typer, tree, sym))
317+
def pluginsTypeSigAccessor(tpe: Type, typer: Typer, tree: ValDef, sym: Symbol): Type = invoke(new CumulativeOp[Type] {
318+
def default = tpe
319+
def accumulate = (tpe, p) => p.pluginsTypeSigAccessor(tpe, typer, tree, sym)
320+
})
197321

198322
/** @see AnalyzerPlugin.canAdaptAnnotations */
199-
def canAdaptAnnotations(tree: Tree, typer: Typer, mode: Mode, pt: Type): Boolean = {
323+
def canAdaptAnnotations(tree: Tree, typer: Typer, mode: Mode, pt: Type): Boolean = invoke(new CumulativeOp[Boolean] {
200324
// support deprecated methods in annotation checkers
201-
val annotCheckersExists = global.canAdaptAnnotations(tree, mode, pt)
202-
annotCheckersExists || {
203-
if (analyzerPlugins.isEmpty) false
204-
else analyzerPlugins.exists(plugin =>
205-
plugin.isActive() && plugin.canAdaptAnnotations(tree, typer, mode, pt))
206-
}
207-
}
325+
def default = global.canAdaptAnnotations(tree, mode, pt)
326+
def accumulate = (curr, p) => curr || p.canAdaptAnnotations(tree, typer, mode, pt)
327+
})
208328

209329
/** @see AnalyzerPlugin.adaptAnnotations */
210-
def adaptAnnotations(tree: Tree, typer: Typer, mode: Mode, pt: Type): Tree = {
330+
def adaptAnnotations(tree: Tree, typer: Typer, mode: Mode, pt: Type): Tree = invoke(new CumulativeOp[Tree] {
211331
// support deprecated methods in annotation checkers
212-
val annotCheckersTree = global.adaptAnnotations(tree, mode, pt)
213-
if (analyzerPlugins.isEmpty) annotCheckersTree
214-
else analyzerPlugins.foldLeft(annotCheckersTree)((tree, plugin) =>
215-
if (!plugin.isActive()) tree else plugin.adaptAnnotations(tree, typer, mode, pt))
216-
}
332+
def default = global.adaptAnnotations(tree, mode, pt)
333+
def accumulate = (tree, p) => p.adaptAnnotations(tree, typer, mode, pt)
334+
})
217335

218336
/** @see AnalyzerPlugin.pluginsTypedReturn */
219-
def pluginsTypedReturn(tpe: Type, typer: Typer, tree: Return, pt: Type): Type = {
220-
val annotCheckersType = adaptTypeOfReturn(tree.expr, pt, tpe)
221-
if (analyzerPlugins.isEmpty) annotCheckersType
222-
else analyzerPlugins.foldLeft(annotCheckersType)((tpe, plugin) =>
223-
if (!plugin.isActive()) tpe else plugin.pluginsTypedReturn(tpe, typer, tree, pt))
337+
def pluginsTypedReturn(tpe: Type, typer: Typer, tree: Return, pt: Type): Type = invoke(new CumulativeOp[Type] {
338+
def default = adaptTypeOfReturn(tree.expr, pt, tpe)
339+
def accumulate = (tpe, p) => p.pluginsTypedReturn(tpe, typer, tree, pt)
340+
})
341+
342+
/** A list of registered macro plugins */
343+
private var macroPlugins: List[MacroPlugin] = Nil
344+
345+
/** Registers a new macro plugin */
346+
def addMacroPlugin(plugin: MacroPlugin) {
347+
if (!macroPlugins.contains(plugin))
348+
macroPlugins = plugin :: macroPlugins
349+
}
350+
351+
private abstract class NonCumulativeOp[T] {
352+
def position: Position
353+
def description: String
354+
def default: T
355+
def custom(plugin: MacroPlugin): Option[T]
356+
}
357+
358+
private def invoke[T](op: NonCumulativeOp[T]): T = {
359+
if (macroPlugins.isEmpty) op.default
360+
else {
361+
val results = macroPlugins.filter(_.isActive()).map(plugin => (plugin, op.custom(plugin)))
362+
results.flatMap { case (p, Some(result)) => Some((p, result)); case _ => None } match {
363+
case (p1, _) :: (p2, _) :: _ => typer.context.error(op.position, s"both $p1 and $p2 want to ${op.description}"); op.default
364+
case (_, custom) :: Nil => custom
365+
case Nil => op.default
366+
}
367+
}
368+
}
369+
370+
/** @see MacroPlugin.pluginsTypedMacroBody */
371+
def pluginsTypedMacroBody(typer: Typer, ddef: DefDef): Tree = invoke(new NonCumulativeOp[Tree] {
372+
def position = ddef.pos
373+
def description = "typecheck this macro definition"
374+
def default = standardTypedMacroBody(typer, ddef)
375+
def custom(plugin: MacroPlugin) = plugin.pluginsTypedMacroBody(typer, ddef)
376+
})
377+
378+
/** @see MacroPlugin.pluginsMacroExpand */
379+
def pluginsMacroExpand(typer: Typer, expandee: Tree, mode: Mode, pt: Type): Tree = invoke(new NonCumulativeOp[Tree] {
380+
def position = expandee.pos
381+
def description = "expand this macro application"
382+
def default = standardMacroExpand(typer, expandee, mode, pt)
383+
def custom(plugin: MacroPlugin) = plugin.pluginsMacroExpand(typer, expandee, mode, pt)
384+
})
385+
386+
/** @see MacroPlugin.pluginsMacroArgs */
387+
def pluginsMacroArgs(typer: Typer, expandee: Tree): MacroArgs = invoke(new NonCumulativeOp[MacroArgs] {
388+
def position = expandee.pos
389+
def description = "compute macro arguments for this macro application"
390+
def default = standardMacroArgs(typer, expandee)
391+
def custom(plugin: MacroPlugin) = plugin.pluginsMacroArgs(typer, expandee)
392+
})
393+
394+
/** @see MacroPlugin.pluginsMacroRuntime */
395+
def pluginsMacroRuntime(expandee: Tree): MacroRuntime = invoke(new NonCumulativeOp[MacroRuntime] {
396+
def position = expandee.pos
397+
def description = "compute macro runtime for this macro application"
398+
def default = standardMacroRuntime(expandee)
399+
def custom(plugin: MacroPlugin) = plugin.pluginsMacroRuntime(expandee)
400+
})
401+
402+
/** @see MacroPlugin.pluginsEnterSym */
403+
def pluginsEnterSym(namer: Namer, tree: Tree): Context =
404+
if (macroPlugins.isEmpty) namer.standardEnterSym(tree)
405+
else invoke(new NonCumulativeOp[Context] {
406+
def position = tree.pos
407+
def description = "enter a symbol for this tree"
408+
def default = namer.standardEnterSym(tree)
409+
def custom(plugin: MacroPlugin) = {
410+
val hasExistingSym = tree.symbol != NoSymbol
411+
val result = plugin.pluginsEnterSym(namer, tree)
412+
if (result && hasExistingSym) Some(namer.context)
413+
else if (result && tree.isInstanceOf[Import]) Some(namer.context.make(tree))
414+
else if (result) Some(namer.context)
415+
else None
416+
}
417+
})
418+
419+
/** @see MacroPlugin.pluginsEnsureCompanionObject */
420+
def pluginsEnsureCompanionObject(namer: Namer, cdef: ClassDef, creator: ClassDef => Tree = companionModuleDef(_)): Symbol = invoke(new NonCumulativeOp[Symbol] {
421+
def position = cdef.pos
422+
def description = "enter a companion symbol for this tree"
423+
def default = namer.standardEnsureCompanionObject(cdef, creator)
424+
def custom(plugin: MacroPlugin) = plugin.pluginsEnsureCompanionObject(namer, cdef, creator)
425+
})
426+
427+
/** @see MacroPlugin.pluginsEnterStats */
428+
def pluginsEnterStats(typer: Typer, stats: List[Tree]): List[Tree] = {
429+
// performance opt
430+
if (macroPlugins.isEmpty) stats
431+
else macroPlugins.foldLeft(stats)((current, plugin) =>
432+
if (!plugin.isActive()) current else plugin.pluginsEnterStats(typer, stats))
224433
}
225434
}

src/compiler/scala/tools/nsc/typechecker/ContextErrors.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,11 @@ trait ContextErrors {
730730
issueNormalTypeError(expandee, s"macro in $role role can only expand into $allowedExpansions")
731731
}
732732

733+
def MacroIncompatibleEngineError(macroEngine: String) = {
734+
val message = s"macro cannot be expanded, because it was compiled by an incompatible macro engine $macroEngine"
735+
issueNormalTypeError(lastTreeToTyper, message)
736+
}
737+
733738
case object MacroExpansionException extends Exception with scala.util.control.ControlThrowable
734739

735740
protected def macroExpansionError(expandee: Tree, msg: String, pos: Position = NoPosition) = {

0 commit comments

Comments
 (0)