ammonite.compiler.CompilerLifecycleManager.scala Maven / Gradle / Ivy
The newest version!
package ammonite.compiler
import ammonite.compiler.iface.{
Compiler => ICompiler,
CompilerLifecycleManager => ICompilerLifecycleManager,
Preprocessor
}
import ammonite.util.Util._
import ammonite.util.{Classpath, Frame, Printer}
import java.nio.file.Path
import scala.collection.mutable
import scala.reflect.io.VirtualDirectory
import scala.tools.nsc.Settings
/**
* Wraps up the `Compiler` and `Pressy`, ensuring that they get properly
* initialized before use. Mostly deals with ensuring the object lifecycles
* are properly dealt with; `Compiler` and `Pressy` are the ones which deal
* with the compiler's nasty APIs
*
* Exposes a simple API where you can just call methods like `compilerClass`
* `configureCompiler` any-how and not worry about ensuring the necessary
* compiler objects are initialized, or worry about initializing them more
* than necessary
*/
class CompilerLifecycleManager(
rtCacheDir: Option[Path],
headFrame: => ammonite.util.Frame,
dependencyCompleteOpt: => Option[String => (Int, Seq[String])],
classPathWhitelist: Set[Seq[String]],
initialClassLoader: ClassLoader,
val outputDir: Option[Path],
initialSettings: Seq[String]
) extends ICompilerLifecycleManager {
def scalaVersion = scala.util.Properties.versionNumberString
private[this] object Internal {
val dynamicClasspath = new VirtualDirectory("(memory)", None)
var compiler: Compiler = null
val onCompilerInit = mutable.Buffer.empty[scala.tools.nsc.Global => Unit]
val onSettingsInit = mutable.Buffer.empty[scala.tools.nsc.Settings => Unit]
var preConfiguredSettingsChanged: Boolean = false
var pressy: Pressy = _
var compilationCount = 0
var (lastFrame, lastFrameVersion) = (headFrame, headFrame.version)
}
import Internal._
// Public to expose it in the REPL so people can poke at it at runtime
// Not for use within Ammonite! Use one of the other methods to ensure
// that `Internal.compiler` is properly initialized before use.
def compiler = Internal.compiler
def compilationCount = Internal.compilationCount
def pressy: Pressy = Internal.pressy
def preprocess(fileName: String) = synchronized {
init()
compiler.preprocessor(fileName)
}
// We lazily force the compiler to be re-initialized by setting the
// compilerStale flag. Otherwise, if we re-initialized the compiler eagerly,
// we end up sometimes re-initializing it multiple times unnecessarily before
// it gets even used once. Empirically, this cuts down the number of compiler
// re-initializations by about 2/3, each of which costs about 30ms and
// probably creates a pile of garbage
def init(force: Boolean = false) = synchronized {
if (
compiler == null ||
(headFrame ne lastFrame) ||
headFrame.version != lastFrameVersion ||
Internal.preConfiguredSettingsChanged ||
force
) {
lastFrame = headFrame
lastFrameVersion = headFrame.version
// Note we not only make a copy of `settings` to pass to the compiler,
// we also make a *separate* copy to pass to the presentation compiler.
// Otherwise activating autocomplete makes the presentation compiler mangle
// the shared settings and makes the main compiler sad
val settings = Option(compiler).fold(new Settings)(_.compiler.settings.copy)
val (success, trailingSettings) =
settings.processArguments(initialSettings.toList, processAll = true)
if (!success)
System.err.println(s"Error processing initial settings ${initialSettings.mkString(" ")}")
onSettingsInit.foreach(_(settings))
val initialClassPath = Classpath.classpath(initialClassLoader, rtCacheDir)
val headFrameClassPath =
Classpath.classpath(headFrame.classloader, rtCacheDir)
Internal.compiler = Compiler(
headFrameClassPath,
dynamicClasspath,
outputDir,
headFrame.classloader,
headFrame.pluginClassloader,
() => shutdownPressy(),
None,
settings,
classPathWhitelist,
initialClassPath
)
onCompilerInit.foreach(_(compiler.compiler))
// Pressy is lazy, so the actual presentation compiler won't get instantiated
// & initialized until one of the methods on it is actually used
Internal.pressy = Pressy(
headFrameClassPath,
dynamicClasspath,
headFrame.classloader,
settings.copy(),
dependencyCompleteOpt,
classPathWhitelist,
initialClassPath
)
Internal.preConfiguredSettingsChanged = false
}
}
def complete(offset: Int, previousImports: String, snippet: String) = synchronized {
init()
pressy.complete(offset, previousImports, snippet)
}
def compileClass(
processed: Preprocessor.Output,
printer: Printer,
fileName: String
): Option[ICompiler.Output] = synchronized {
// Enforce the invariant that every piece of code Ammonite ever compiles,
// gets run within the `ammonite` package. It's further namespaced into
// things like `ammonite.$file` or `ammonite.$sess`, but it has to be
// within `ammonite`
assert(processed.code.trim.startsWith("package ammonite"))
init()
val compiled = compiler.compile(
processed.code.getBytes(scala.util.Properties.sourceEncoding),
printer,
processed.prefixCharLength,
processed.userCodeNestingLevel,
fileName
)
Internal.compilationCount += 1
compiled
}
def configureCompiler(callback: scala.tools.nsc.Global => Unit) = synchronized {
onCompilerInit.append(callback)
if (compiler != null) {
callback(compiler.compiler)
}
}
def preConfigureCompiler(callback: scala.tools.nsc.Settings => Unit) =
synchronized {
onSettingsInit.append(callback)
preConfiguredSettingsChanged = true
}
def addToClasspath(classFiles: ClassFiles) = synchronized {
Compiler.addToClasspath(classFiles, dynamicClasspath, outputDir)
}
// Not synchronized, since it's part of the exit sequence that needs to run
// if the repl exits while the warmup code is compiling
def shutdownPressy() = {
if (pressy != null) pressy.shutdownPressy()
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy