Skip to content

Commit

Permalink
Overhaul integration with front end macros
Browse files Browse the repository at this point in the history
  - The state machine generated by the front end macro must Now
    implement a small set of methods as a facade over the
    particular Future/Awaitable/Task type being used. This
    replaces the AST factory methods in `FutureSystem`
  - Remove intrinsic implemnentation for scala-async's async
    macro. Show what it will need to do in a test implementation,
    `s.t.n.partest.Async` and update all test cases to use this.
  - Add a heuristic to interpret `@compileTimeOnly` annotated
    methods to register `await` methods. Defer the check in refchecks
    for all of these and instead check that no references survive
    the async phase.
  - Refactor the test implementations of async front ends to
    share an interface for the state machine. We don't ship
    this but it could be copy/pasted into third party integrations.
  - Add a test integration with Java's CompletableFuture.
  - Expose a method through macro `Internals` API to let
    front ends mark a method in the state machine for async
    translation.
  • Loading branch information
retronym committed Mar 20, 2020
1 parent 5fd609e commit 0dc0333
Show file tree
Hide file tree
Showing 108 changed files with 649 additions and 672 deletions.
4 changes: 4 additions & 0 deletions build.sbt
Expand Up @@ -370,6 +370,10 @@ val mimaFilterSettings = Seq {
ProblemFilters.exclude[DirectMissingMethodProblem]("scala.reflect.macros.Attachments.removeElement"),
ProblemFilters.exclude[DirectMissingMethodProblem]("scala.reflect.macros.Attachments.addElement"),
ProblemFilters.exclude[DirectMissingMethodProblem]("scala.reflect.macros.Attachments.containsElement"),

ProblemFilters.exclude[DirectMissingMethodProblem]("scala.reflect.runtime.Settings.async"),

ProblemFilters.exclude[DirectMissingMethodProblem]("scala.reflect.api.Internals#InternalApi.markForAsyncTransform"),
)
}

Expand Down
2 changes: 1 addition & 1 deletion project/ScalaOptionParser.scala
Expand Up @@ -82,7 +82,7 @@ object ScalaOptionParser {
}

// TODO retrieve this data programmatically, ala https://github.com/scala/scala-tool-support/blob/master/bash-completion/src/main/scala/BashCompletion.scala
private def booleanSettingNames = List("-X", "-Xcheckinit", "-Xdev", "-Xdisable-assertions", "-Xexperimental", "-Xfatal-warnings", "-Xfull-lubs", "-Xfuture", "-Xlog-free-terms", "-Xlog-free-types", "-Xlog-implicit-conversions", "-Xlog-implicits", "-Xlog-reflective-calls",
private def booleanSettingNames = List("-X", "-Xasync", "-Xcheckinit", "-Xdev", "-Xdisable-assertions", "-Xexperimental", "-Xfatal-warnings", "-Xfull-lubs", "-Xfuture", "-Xlog-free-terms", "-Xlog-free-types", "-Xlog-implicit-conversions", "-Xlog-implicits", "-Xlog-reflective-calls",
"-Xno-forwarders", "-Xno-patmat-analysis", "-Xno-uescape", "-Xnojline", "-Xprint-pos", "-Xprint-types", "-Xprompt", "-Xresident", "-Xshow-phases", "-Xstrict-inference", "-Xverify", "-Y",
"-Ybreak-cycles", "-Ydebug", "-Ycompact-trees", "-YdisableFlatCpCaching", "-Ydoc-debug",
"-Yide-debug", "-Yinfer-argument-types",
Expand Down
5 changes: 4 additions & 1 deletion src/compiler/scala/reflect/macros/contexts/Internals.scala
Expand Up @@ -55,5 +55,8 @@ trait Internals extends scala.tools.nsc.transform.TypingTransformers {
val trans = new HofTypingTransformer(transformer)
trans.atOwner(owner)(trans.transform(tree))
}
override def markForAsyncTransform(owner: Symbol, method: DefDef, awaitSymbol: Symbol, config: Map[String, AnyRef]): DefDef = {
global.async.markForAsyncTransform(owner, method, awaitSymbol, config)
}
}
}
}
9 changes: 8 additions & 1 deletion src/compiler/scala/tools/nsc/Global.scala
Expand Up @@ -374,6 +374,12 @@ class Global(var currentSettings: Settings, reporter0: Reporter)
getSourceFile(f)
}

override lazy val internal: Internal = new SymbolTableInternal {
override def markForAsyncTransform(owner: Symbol, method: DefDef, awaitSymbol: Symbol, config: Map[String, AnyRef]): DefDef = {
async.markForAsyncTransform(owner, method, awaitSymbol, config)
}
}

lazy val loaders = new {
val global: Global.this.type = Global.this
val platform: Global.this.platform.type = Global.this.platform
Expand Down Expand Up @@ -1011,7 +1017,7 @@ class Global(var currentSettings: Settings, reporter0: Reporter)
definitions.isDefinitionsInitialized
&& rootMirror.isMirrorInitialized
)
override def isPastTyper = isPast(currentRun.typerPhase)
override def isPastTyper = globalPhase != null && isPast(currentRun.typerPhase)
def isPast(phase: Phase) = (
(curRun ne null)
&& isGlobalInitialized // defense against init order issues
Expand Down Expand Up @@ -1348,6 +1354,7 @@ class Global(var currentSettings: Settings, reporter0: Reporter)
def runIsAt(ph: Phase) = globalPhase.id == ph.id
def runIsAtOptimiz = runIsAt(jvmPhase)

firstPhase.iterator.foreach(_.init())
isDefined = true

// ----------- Units and top-level classes and objects --------
Expand Down
1 change: 1 addition & 0 deletions src/compiler/scala/tools/nsc/settings/ScalaSettings.scala
Expand Up @@ -113,6 +113,7 @@ trait ScalaSettings extends AbsScalaSettings
* -X "Advanced" settings
*/
val Xhelp = BooleanSetting ("-X", "Print a synopsis of advanced options.")
val async = BooleanSetting ("-Xasync", "Enable the async phase for scala.async.Async.{async,await}.")
val checkInit = BooleanSetting ("-Xcheckinit", "Wrap field accessors to throw an exception on uninitialized access.")
val developer = BooleanSetting ("-Xdev", "Indicates user is a developer - issue warnings about anything which seems amiss")
val noassertions = BooleanSetting ("-Xdisable-assertions", "Generate no assertions or assumptions.") andThen (flag =>
Expand Down
Expand Up @@ -392,5 +392,3 @@ private[async] trait AnfTransform extends TransformUtils {
}
}
}

object SyntheticBindVal
131 changes: 0 additions & 131 deletions src/compiler/scala/tools/nsc/transform/async/AsyncEarlyExpansion.scala

This file was deleted.

94 changes: 62 additions & 32 deletions src/compiler/scala/tools/nsc/transform/async/AsyncPhase.scala
Expand Up @@ -13,56 +13,85 @@
package scala.tools.nsc.transform.async

import scala.collection.mutable
import scala.tools.nsc.transform.async.user.FutureSystem
import scala.tools.nsc.transform.{Transform, TypingTransformers}
import scala.reflect.internal.util.SourceFile

abstract class AsyncPhase extends Transform with TypingTransformers with AnfTransform with AsyncAnalysis with Lifter with LiveVariables {
self =>
import global._

private[async] var currentTransformState: AsyncTransformState[global.type] = _
private[async] var currentTransformState: AsyncTransformState = _
private[async] val asyncNames = new AsyncNames[global.type](global)
protected[async] val tracing = new Tracing

val phaseName: String = "async"
override def enabled: Boolean = settings.async

private final class FutureSystemAttachment(val system: FutureSystem) extends PlainAttachment
private final case class AsyncAttachment(awaitSymbol: Symbol, postAnfTransform: Block => Block, stateDiagram: ((Symbol, Tree) => Option[String => Unit])) extends PlainAttachment

// Optimization: avoid the transform altogether if there are no async blocks in a unit.
private val units = perRunCaches.newSet[CompilationUnit]()
final def addFutureSystemAttachment(unit: CompilationUnit, method: Tree, system: FutureSystem): method.type = {
units += unit
method.updateAttachment(new FutureSystemAttachment(system))
private val sourceFilesToTransform = perRunCaches.newSet[SourceFile]()
private val awaits: mutable.Set[Symbol] = perRunCaches.newSet[Symbol]()

/**
* Mark the given method as requiring an async transform.
*/
final def markForAsyncTransform(owner: Symbol, method: DefDef, awaitMethod: Symbol,
config: Map[String, AnyRef]): DefDef = {
val pos = owner.pos
if (!settings.async)
reporter.warning(pos, s"${settings.async.name} must be enabled for async transformation.")
sourceFilesToTransform += pos.source
val postAnfTransform = config.getOrElse("postAnfTransform", (x: Block) => x).asInstanceOf[Block => Block]
val stateDiagram = config.getOrElse("stateDiagram", (sym: Symbol, tree: Tree) => None).asInstanceOf[(Symbol, Tree) => Option[String => Unit]]
method.updateAttachment(new AsyncAttachment(awaitMethod, postAnfTransform, stateDiagram))
deriveDefDef(method) { rhs =>
Block(rhs.updateAttachment(SuppressPureExpressionWarning), Literal(Constant(())))
}.updateAttachment(ChangeOwnerAttachment(owner))
}

protected object macroExpansion extends AsyncEarlyExpansion {
val global: self.global.type = self.global
}

import treeInfo.Applied
def fastTrackEntry: (Symbol, PartialFunction[Applied, scala.reflect.macros.contexts.Context { val universe: self.global.type } => Tree]) =
(currentRun.runDefinitions.Async_async, {
// def async[T](body: T)(implicit execContext: ExecutionContext): Future[T] = macro ???
case app@Applied(_, _, List(asyncBody :: Nil, execContext :: Nil)) =>
c => c.global.async.macroExpansion.apply(c.callsiteTyper, asyncBody, execContext, asyncBody.tpe)
})

def newTransformer(unit: CompilationUnit): Transformer = new AsyncTransformer(unit)

private def compileTimeOnlyPrefix: String = "[async] "

/** Should refchecks defer reporting `@compileTimeOnly` errors for `sym` and instead let this phase issue the warning
* if they survive the async tranform? */
private[scala] def deferCompileTimeOnlyError(sym: Symbol): Boolean = settings.async && {
awaits.contains(sym) || {
val msg = sym.compileTimeOnlyMessage.getOrElse("")
val shouldDefer =
msg.startsWith(compileTimeOnlyPrefix) || (sym.name == nme.await) && msg.contains("must be enclosed") && sym.owner.info.member(nme.async) != NoSymbol
if (shouldDefer) awaits += sym
shouldDefer
}
}

// TOOD: figure out how to make the root-level async built-in macro sufficiently configurable:
// replace the ExecutionContext implicit arg with an AsyncContext implicit that also specifies the type of the Future/Awaitable/Node/...?
final class AsyncTransformer(unit: CompilationUnit) extends TypingTransformer(unit) {
private lazy val liftableMap = new mutable.AnyRefMap[Symbol, (Symbol, List[Tree])]()

override def transformUnit(unit: CompilationUnit): Unit =
if (units.contains(unit)) super.transformUnit(unit)
override def transformUnit(unit: CompilationUnit): Unit = {
if (settings.async) {
if (sourceFilesToTransform.contains(unit.source)) super.transformUnit(unit)
if (awaits.exists(_.isInitialized)) {
unit.body.foreach {
case tree: RefTree if tree.symbol != null && awaits.contains(tree.symbol) =>
val sym = tree.symbol
val msg = sym.compileTimeOnlyMessage.getOrElse(s"`${sym.decodedName}` must be enclosed in an `async` block").stripPrefix(compileTimeOnlyPrefix)
global.reporter.error(tree.pos, msg)
case _ =>
}
}
}
}

// Together, these transforms below target this tree shaps
// {
// class $STATE_MACHINE extends ... {
// def $APPLY_METHOD(....) = {
// ...
// }.updateAttachment(FutureSystemAttachment(...))
// }.updateAttachment(AsyncAttachment(...))
// }
// }
//
Expand All @@ -74,16 +103,16 @@ abstract class AsyncPhase extends Transform with TypingTransformers with AnfTran
case cd: ClassDef if liftableMap.contains(cd.symbol) =>
val (applySym, liftedTrees) = liftableMap.remove(cd.symbol).get
val liftedSyms = liftedTrees.iterator.map(_.symbol).toSet
val cd1 = atOwner(tree.symbol) {
val cd1 = atOwner(cd.symbol) {
deriveClassDef(cd)(impl => {
deriveTemplate(impl)(liftedTrees ::: _)
})
}
assert(localTyper.context.owner == cd.symbol.owner)
new UseFields(localTyper, cd.symbol, applySym, liftedSyms).transform(cd1)

case dd: DefDef if tree.hasAttachment[FutureSystemAttachment] =>
val futureSystem = tree.getAndRemoveAttachment[FutureSystemAttachment].get.system
case dd: DefDef if dd.hasAttachment[AsyncAttachment] =>
val asyncAttachment = dd.getAndRemoveAttachment[AsyncAttachment].get
val asyncBody = (dd.rhs: @unchecked) match {
case blk@Block(stats, Literal(Constant(()))) => treeCopy.Block(blk, stats.init, stats.last).setType(stats.last.tpe)
}
Expand All @@ -92,7 +121,8 @@ abstract class AsyncPhase extends Transform with TypingTransformers with AnfTran
atOwner(dd, dd.symbol) {
val trSym = dd.vparamss.head.head.symbol
val saved = currentTransformState
currentTransformState = new AsyncTransformState[global.type](global, futureSystem, this, trSym, asyncBody.tpe)
currentTransformState = new AsyncTransformState(asyncAttachment.awaitSymbol,
asyncAttachment.postAnfTransform, asyncAttachment.stateDiagram, this, trSym, asyncBody.tpe)
try {
val (newRhs, liftableFields) = asyncTransform(asyncBody)
liftableMap(dd.symbol.owner) = (dd.symbol, liftableFields)
Expand All @@ -101,13 +131,13 @@ abstract class AsyncPhase extends Transform with TypingTransformers with AnfTran
currentTransformState = saved
}
}
case tree => tree
case tree =>
tree
}

private def asyncTransform(asyncBody: Tree): (Tree, List[Tree]) = {
val transformState = currentTransformState
import transformState.applySym
val futureSystemOps = transformState.ops

val asyncPos = asyncBody.pos

Expand All @@ -119,7 +149,7 @@ abstract class AsyncPhase extends Transform with TypingTransformers with AnfTran
// Transform to A-normal form:
// - no await calls in qualifiers or arguments,
// - if/match only used in statement position.
val anfTree: Block = futureSystemOps.postAnfTransform(new AnfTransformer(localTyper).apply(asyncBody))
val anfTree: Block = transformState.postAnfTransform(new AnfTransformer(localTyper).apply(asyncBody))

// The ANF transform re-parents some trees, so the previous traversal to mark ancestors of
// await is no longer reliable. Clear previous results and run it again for use in the `buildAsyncBlock`.
Expand All @@ -144,10 +174,10 @@ abstract class AsyncPhase extends Transform with TypingTransformers with AnfTran
val applyBody = atPos(asyncPos)(asyncBlock.onCompleteHandler)

// Logging
if (settings.debug.value && shouldLogAtThisPhase)
if ((settings.debug.value && shouldLogAtThisPhase))
logDiagnostics(anfTree, asyncBlock, asyncBlock.asyncStates.map(_.toString))
// Offer the future system a change to produce the .dot diagram
futureSystemOps.dot(applySym, asyncBody).foreach(f => f(asyncBlock.toDot))
// Offer async frontends a change to produce the .dot diagram
transformState.dotDiagram(applySym, asyncBody).foreach(f => f(asyncBlock.toDot))

cleanupContainsAwaitAttachments(applyBody)

Expand Down

0 comments on commit 0dc0333

Please sign in to comment.