Skip to content

Fix #1519: Port compiler Plugin from scalac #3438

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 27 commits into from
Mar 26, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9e64cd8
WIP - port plugin architecture from scalac
liufengyun Nov 3, 2017
3868485
Create test infrastructure for plugins
liufengyun Nov 3, 2017
3d67c33
refine error message
liufengyun Nov 3, 2017
7205c44
support both ordering and research plugin
liufengyun Nov 16, 2017
a9c2889
fix syntax error after rebase
liufengyun Nov 16, 2017
38ed11a
add non-research plugin tests
liufengyun Nov 16, 2017
a1a8c12
refactor ordering algorithm for unit test
liufengyun Nov 17, 2017
b05aa22
unit test plugin schedule algorithm
liufengyun Nov 17, 2017
7fccb8e
make scheduling deterministic
liufengyun Nov 17, 2017
cb4d90e
make plugin pass bootstrapped CI
liufengyun Nov 17, 2017
4510b18
add failing test
liufengyun Nov 17, 2017
48483cb
Make scheduler deterministic by propagating odering constraints
liufengyun Nov 17, 2017
45ea80f
add transitive ordering constraint example
liufengyun Nov 17, 2017
44f2a32
add sbt plugin test
liufengyun Nov 17, 2017
7fa7e72
remove unnecessary changes to vulpix
liufengyun Nov 17, 2017
bcee0af
fix rebase conflict
liufengyun Dec 19, 2017
5aac55d
fix path after rebase
liufengyun Mar 9, 2018
90d707e
fix typo in tests
liufengyun Mar 19, 2018
c49ad7f
split Plugin to ResearchPlugin & StandardPlugin
liufengyun Mar 19, 2018
334636e
fix more review feedback
liufengyun Mar 20, 2018
7c3e289
add comment for primitive phases
liufengyun Mar 20, 2018
2737820
use Try.flatMap with List.map
liufengyun Mar 20, 2018
2212dc0
use string names for runsAfter
liufengyun Mar 20, 2018
d2a63fb
add plugin authors [CI SKIP]
liufengyun Mar 20, 2018
32e6faa
remove xml file
liufengyun Mar 20, 2018
0e253ef
use plugin.properties
liufengyun Mar 22, 2018
f1a6923
small tweak
liufengyun Mar 23, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,7 @@ The majority of the dotty codebase is new code, with the exception of the compon
> the [compiler bridge in sbt 0.13](https://github.com/sbt/sbt/tree/0.13/compile/interface/src/main/scala/xsbt),
> but has been heavily adapted and refactored.
> Original authors were Mark Harrah, Grzegorz Kossakowski, Martin Duhemm, Adriaan Moors and others.

`dotty.tools.dotc.plugins`

> Adapted from [scala/scala](https://github.com/scala/scala) with some modifications. They were originally authored by Lex Spoon, Som Snytt, Adriaan Moors, Paul Phillips and others.
3 changes: 2 additions & 1 deletion compiler/src/dotty/tools/dotc/Run.scala
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,8 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
if (ctx.settings.YtestPickler.value) List("pickler")
else ctx.settings.YstopAfter.value

val phases = ctx.squashPhases(ctx.phasePlan,
val pluginPlan = ctx.addPluginPhases(ctx.phasePlan)
val phases = ctx.squashPhases(pluginPlan,
ctx.settings.Yskip.value, ctx.settings.YstopBefore.value, stopAfter, ctx.settings.Ycheck.value)
ctx.usePhases(phases)

Expand Down
3 changes: 2 additions & 1 deletion compiler/src/dotty/tools/dotc/config/CompilerCommand.scala
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,15 @@ object CompilerCommand extends DotClass {

def shouldStopWithInfo = {
import settings._
Set(help, Xhelp, Yhelp) exists (_.value)
Set(help, Xhelp, Yhelp, showPlugins) exists (_.value)
}

def infoMessage: String = {
import settings._
if (help.value) usageMessage
else if (Xhelp.value) xusageMessage
else if (Yhelp.value) yusageMessage
else if (showPlugins.value) ctx.pluginDescriptions
else ""
}

Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/config/Printers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ object Printers {
val overload: Printer = noPrinter
val patmatch: Printer = noPrinter
val pickling: Printer = noPrinter
val plugins: Printer = noPrinter
val simplify: Printer = noPrinter
val subtyping: Printer = noPrinter
val transforms: Printer = noPrinter
Expand Down
8 changes: 8 additions & 0 deletions compiler/src/dotty/tools/dotc/config/ScalaSettings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ class ScalaSettings extends Settings.SettingGroup {
val printTasty = BooleanSetting("-print-tasty", "Prints the raw tasty when decompiling.")
val printLines = BooleanSetting("-print-lines", "Show source code line numbers.")

/** Plugin-related setting */
val plugin = MultiStringSetting ("-Xplugin", "paths", "Load a plugin from each classpath.")
val disable = MultiStringSetting ("-Xplugin-disable", "plugin", "Disable plugins by name.")
val require = MultiStringSetting ("-Xplugin-require", "plugin", "Abort if a named plugin is not loaded.")
val showPlugins = BooleanSetting ("-Xplugin-list", "Print a synopsis of loaded plugins.")
val pluginsDir = StringSetting ("-Xpluginsdir", "path", "Path to search for plugin archives.", Defaults.scalaPluginPath)
val pluginOptions = MultiStringSetting ("-P", "plugin:opt", "Pass an option to a plugin, e.g. -P:<plugin>:<opt>")

/** -X "Advanced" settings
*/
val Xhelp = BooleanSetting("-X", "Print a synopsis of advanced options.")
Expand Down
2 changes: 2 additions & 0 deletions compiler/src/dotty/tools/dotc/core/Contexts.scala
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import dotty.tools.dotc.profile.Profiler
import util.Property.Key
import util.Store
import xsbti.AnalysisCallback
import plugins._

object Contexts {

Expand Down Expand Up @@ -76,6 +77,7 @@ object Contexts {
with SymDenotations
with Reporting
with NamerContextOps
with Plugins
with Cloneable { thiscontext =>
implicit def ctx: Context = this

Expand Down
21 changes: 6 additions & 15 deletions compiler/src/dotty/tools/dotc/core/Phases.scala
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ object Phases {
final def squashPhases(phasess: List[List[Phase]],
phasesToSkip: List[String], stopBeforePhases: List[String], stopAfterPhases: List[String], YCheckAfter: List[String]): List[Phase] = {
val squashedPhases = ListBuffer[Phase]()
var prevPhases: Set[Class[_ <: Phase]] = Set.empty
var prevPhases: Set[String] = Set.empty
val YCheckAll = YCheckAfter.contains("all")

var stop = false
Expand All @@ -99,7 +99,6 @@ object Phases {
val filteredPhaseBlock = filteredPhases(i)
val phaseToAdd =
if (filteredPhaseBlock.length > 1) {
val phasesInBlock: Set[String] = filteredPhaseBlock.map(_.phaseName).toSet
for (phase <- filteredPhaseBlock) {
phase match {
case p: MiniPhase =>
Expand All @@ -112,11 +111,11 @@ object Phases {
}
}
val superPhase = new MegaPhase(filteredPhaseBlock.asInstanceOf[List[MiniPhase]].toArray)
prevPhases ++= filteredPhaseBlock.map(_.getClazz)
prevPhases ++= filteredPhaseBlock.map(_.phaseName)
superPhase
} else { // block of a single phase, no squashing
val phase = filteredPhaseBlock.head
prevPhases += phase.getClazz
prevPhases += phase.phaseName
phase
}
squashedPhases += phaseToAdd
Expand Down Expand Up @@ -147,7 +146,7 @@ object Phases {

phases = (NoPhase :: flatPhases.toList ::: new TerminalPhase :: Nil).toArray
setSpecificPhases()
var phasesAfter:Set[Class[_ <: Phase]] = Set.empty
var phasesAfter: Set[String] = Set.empty
nextDenotTransformerId = new Array[Int](phases.length)
denotTransformers = new Array[DenotTransformer](phases.length)

Expand All @@ -161,7 +160,7 @@ object Phases {
val unmetPrecedeRequirements = p.runsAfter -- phasesAfter
assert(unmetPrecedeRequirements.isEmpty,
s"phase ${p} has unmet requirement: ${unmetPrecedeRequirements.mkString(", ")} should precede this phase")
phasesAfter += p.getClazz
phasesAfter += p.phaseName

}
var i = 0
Expand Down Expand Up @@ -281,7 +280,7 @@ object Phases {
def allowsImplicitSearch: Boolean = false

/** List of names of phases that should precede this phase */
def runsAfter: Set[Class[_ <: Phase]] = Set.empty
def runsAfter: Set[String] = Set.empty

/** @pre `isRunnable` returns true */
def run(implicit ctx: Context): Unit
Expand Down Expand Up @@ -393,12 +392,4 @@ object Phases {
def replace(oldPhaseClass: Class[_ <: Phase], newPhases: Phase => List[Phase], current: List[List[Phase]]): List[List[Phase]] =
current.map(_.flatMap(phase =>
if (oldPhaseClass.isInstance(phase)) newPhases(phase) else phase :: Nil))

/** Dotty deviation: getClass yields Class[_], instead of [Class <: <type of receiver>].
* We can get back the old behavior using this decorator. We should also use the same
* trick for standard getClass.
*/
private implicit class getClassDeco[T](val x: T) extends AnyVal {
def getClazz: Class[_ <: T] = x.getClass.asInstanceOf[Class[_ <: T]]
}
}
189 changes: 189 additions & 0 deletions compiler/src/dotty/tools/dotc/plugins/Plugin.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package dotty.tools.dotc
package plugins

import core._
import Contexts._
import Phases._
import dotty.tools.io._
import transform.MegaPhase.MiniPhase

import java.io.InputStream
import java.util.Properties

import scala.collection.mutable
import scala.util.{ Try, Success, Failure }

trait PluginPhase extends MiniPhase {
def runsBefore: Set[String] = Set.empty
}

sealed trait Plugin {
/** The name of this plugin */
def name: String

/** A one-line description of the plugin */
def description: String

/** Is this plugin a research plugin?
*
* Research plugin receives a phase plan and return a new phase plan, while
* non-research plugin returns a list of phases to be inserted.
*/
def research: Boolean = isInstanceOf[ResearchPlugin]

/** A description of this plugin's options, suitable as a response
* to the -help command-line option. Conventionally, the options
* should be listed with the `-P:plugname:` part included.
*/
val optionsHelp: Option[String] = None
}

trait StandardPlugin extends Plugin {
/** Non-research plugins should override this method to return the phases
*
* @param options: commandline options to the plugin, `-P:plugname:opt1,opt2` becomes List(opt1, opt2)
* @return a list of phases to be added to the phase plan
*/
def init(options: List[String]): List[PluginPhase]
}

trait ResearchPlugin extends Plugin {
/** Research plugins should override this method to return the new phase plan
*
* @param options: commandline options to the plugin, `-P:plugname:opt1,opt2` becomes List(opt1, opt2)
* @param plan: the given phase plan
* @return the new phase plan
*/
def init(options: List[String], plan: List[List[Phase]])(implicit ctx: Context): List[List[Phase]]
}

object Plugin {

private val PluginFile = "plugin.properties"

/** Create a class loader with the specified locations plus
* the loader that loaded the Scala compiler.
*/
private def loaderFor(locations: Seq[Path]): ClassLoader = {
val compilerLoader = classOf[Plugin].getClassLoader
val urls = locations map (_.toURL)

new java.net.URLClassLoader(urls.toArray, compilerLoader)
}

type AnyClass = Class[_]

/** Use a class loader to load the plugin class.
*/
def load(classname: String, loader: ClassLoader): Try[AnyClass] = {
import scala.util.control.NonFatal
try {
Success[AnyClass](loader loadClass classname)
} catch {
case NonFatal(e) =>
Failure(new PluginLoadException(classname, s"Error: unable to load class $classname: ${e.getMessage}"))
case e: NoClassDefFoundError =>
Failure(new PluginLoadException(classname, s"Error: class not found: ${e.getMessage} required by $classname"))
}
}

/** Load all plugins specified by the arguments.
* Each location of `paths` must be a valid plugin archive or exploded archive.
* Each of `paths` must define one plugin.
* Each of `dirs` may be a directory containing arbitrary plugin archives.
* Skips all plugins named in `ignoring`.
* A classloader is created to load each plugin.
*/
def loadAllFrom(
paths: List[List[Path]],
dirs: List[Path],
ignoring: List[String]): List[Try[Plugin]] =
{

def fromFile(inputStream: InputStream, path: Path): String = {
val props = new Properties
props.load(inputStream)

val pluginClass = props.getProperty("pluginClass")

if (pluginClass == null) throw new RuntimeException("Bad plugin descriptor: " + path)
else pluginClass
}

def loadDescriptionFromDir(f: Path): Try[String] = {
val path = f / PluginFile
Try(fromFile(new java.io.FileInputStream(path.jpath.toFile), path))
}

def loadDescriptionFromJar(jarp: Path): Try[String] = {
// XXX Return to this once we have more ARM support
def read(is: InputStream) =
if (is == null) throw new PluginLoadException(jarp.path, s"Missing $PluginFile in $jarp")
else fromFile(is, jarp)

val fileEntry = new java.util.jar.JarEntry(PluginFile)
Try(read(new Jar(jarp.jpath.toFile).getEntryStream(fileEntry)))
}

// List[(jar, Try(descriptor))] in dir
def scan(d: Directory) =
d.files.toList sortBy (_.name) filter (Jar isJarOrZip _) map (j => (j, loadDescriptionFromJar(j)))

type PDResults = List[Try[(String, ClassLoader)]]

// scan plugin dirs for jars containing plugins, ignoring dirs with none and other jars
val fromDirs: PDResults = dirs filter (_.isDirectory) flatMap { d =>
scan(d.toDirectory) collect {
case (j, Success(pd)) => Success((pd, loaderFor(Seq(j))))
}
}

// scan jar paths for plugins, taking the first plugin you find.
// a path element can be either a plugin.jar or an exploded dir.
def findDescriptor(ps: List[Path]) = {
def loop(qs: List[Path]): Try[String] = qs match {
case Nil => Failure(new MissingPluginException(ps))
case p :: rest =>
if (p.isDirectory) loadDescriptionFromDir(p.toDirectory) orElse loop(rest)
else if (p.isFile) loadDescriptionFromJar(p.toFile) orElse loop(rest)
else loop(rest)
}
loop(ps)
}

val fromPaths: PDResults = paths map (p => findDescriptor(p) match {
case Success(classname) => Success((classname, loaderFor(p)))
case Failure(e) => Failure(e)
})

val seen = mutable.HashSet[String]()
val enabled = (fromPaths ::: fromDirs) map(_.flatMap {
case (classname, loader) =>
Plugin.load(classname, loader).flatMap { clazz =>
val plugin = instantiate(clazz)
if (seen(classname)) // a nod to scala/bug#7494, take the plugin classes distinctly
Failure(new PluginLoadException(plugin.name, s"Ignoring duplicate plugin ${plugin.name} (${classname})"))
else if (ignoring contains plugin.name)
Failure(new PluginLoadException(plugin.name, s"Disabling plugin ${plugin.name}"))
else {
seen += classname
Success(plugin)
}
}
})
enabled // distinct and not disabled
}

/** Instantiate a plugin class, given the class and
* the compiler it is to be used in.
*/
def instantiate(clazz: AnyClass): Plugin = clazz.newInstance.asInstanceOf[Plugin]
}

class PluginLoadException(val path: String, message: String, cause: Exception) extends Exception(message, cause) {
def this(path: String, message: String) = this(path, message, null)
}

class MissingPluginException(path: String) extends PluginLoadException(path, s"No plugin in path $path") {
def this(paths: List[Path]) = this(paths mkString File.pathSeparator)
}
Loading