Skip to content

Commit 59c64f6

Browse files
committed
Merge pull request #1 from smarter/incremental
Add support for incremental compilation
2 parents 6d722e8 + 674c8ef commit 59c64f6

File tree

487 files changed

+4108
-12
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

487 files changed

+4108
-12
lines changed

bridge/LICENSE

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
Copyright (c) 2015 The dotty-bridge contributors.
1+
Copyright (c) 2015-2016 The dotty-bridge contributors.
2+
All rights reserved.
3+
4+
Some parts of this project are copied or adapted from sbt which is:
5+
Copyright (c) 2008-2016 Typesafe Inc, Mark Harrah, Grzegorz Kossakowski,
6+
Josh Suereth, Indrajit Raychaudhuri, Eugene Yokota, and other contributors.
27
All rights reserved.
38

49
Redistribution and use in source and binary forms, with or without

bridge/README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@ it allows you to compile your code using Dotty instead of Scala 2.
77

88
### Implementation status
99

10-
- [X] Minimal `sbt compile` support
11-
- [ ] Incremental compilation (currently, the full sources will always be recompiled)
10+
- [X] `sbt compile` support, including [incremental recompilation](http://www.scala-sbt.org/0.13/docs/Understanding-Recompilation.html)
1211
- [ ] `sbt console` support (may involve some changes to the Dotty REPL)
13-
- [ ] `sbt doc` support (not possible until we have a Dotty doc tool)
12+
- [ ] `sbt doc` support
1413

1514
### Usage
1615

bridge/build.sbt

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ lazy val root = (project in file(".")).
55
description := "sbt compiler bridge for Dotty",
66
libraryDependencies := Seq(
77
"org.scala-lang" %% "dotty" % "0.1-SNAPSHOT",
8-
"org.scala-sbt" % "interface" % sbtVersion.value
8+
"org.scala-sbt" % "interface" % sbtVersion.value,
9+
"org.scala-sbt" % "api" % sbtVersion.value % "test",
10+
"org.specs2" %% "specs2" % "2.3.11" % "test"
911
),
1012
publishArtifact in packageDoc := false,
1113
version := "0.1-SNAPSHOT",
@@ -14,5 +16,37 @@ lazy val root = (project in file(".")).
1416
scalaVersion := "2.11.5",
1517
// Ideally, the sources should be published with crossPaths := false and the
1618
// binaries with crossPaths := true, but I haven't figured out how to do this.
17-
crossPaths := false
19+
crossPaths := false,
20+
21+
fork in Test := true,
22+
parallelExecution in Test := false
1823
)
24+
25+
// Options for scripted tests
26+
ScriptedPlugin.scriptedSettings
27+
scriptedLaunchOpts := Seq("-Xmx1024m")
28+
scriptedBufferLog := false
29+
// TODO: Use this instead of manually copying DottyInjectedPlugin.scala
30+
// everywhere once https://github.com/sbt/sbt/issues/2601 gets fixed.
31+
/*
32+
scriptedPrescripted := { f =>
33+
IO.write(inj, """
34+
import sbt._
35+
import Keys._
36+
37+
object DottyInjectedPlugin extends AutoPlugin {
38+
override def requires = plugins.JvmPlugin
39+
override def trigger = allRequirements
40+
41+
override val projectSettings = Seq(
42+
scalaVersion := "0.1-SNAPSHOT",
43+
scalacOptions += "-language:Scala2",
44+
scalaBinaryVersion := "2.11",
45+
autoScalaLibrary := false,
46+
libraryDependencies ++= Seq("org.scala-lang" % "scala-library" % "2.11.5"),
47+
scalaCompilerBridgeSource := ("ch.epfl.lamp" % "dotty-bridge" % "0.1-SNAPSHOT" % "component").sources()
48+
)
49+
}
50+
""")
51+
}
52+
*/

bridge/project/scripted.sbt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
libraryDependencies <+= (sbtVersion) { sv =>
2+
"org.scala-sbt" % "scripted-plugin" % sv
3+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package xsbt
2+
3+
import java.net.{URL, URLClassLoader}
4+
5+
/** A classloader to run the compiler
6+
*
7+
* A CompilerClassLoader is constructed from a list of `urls` that need to be on
8+
* the classpath to run the compiler and the classloader used by sbt.
9+
*
10+
* To understand why a custom classloader is needed for the compiler, let us
11+
* describe some alternatives that wouldn't work.
12+
* - `new URLClassLoader(urls)`:
13+
* The compiler contains sbt phases that callback to sbt using the `xsbti.*`
14+
* interfaces. If `urls` does not contain the sbt interfaces we'll get a
15+
* `ClassNotFoundException` in the compiler when we try to use them, if
16+
* `urls` does contain the interfaces we'll get a `ClassCastException` or a
17+
* `LinkageError` because if the same class is loaded by two different
18+
* classloaders, they are considered distinct by the JVM.
19+
* - `new URLClassLoader(urls, sbtLoader)`:
20+
* Because of the JVM delegation model, this means that we will only load
21+
* a class from `urls` if it's not present in the parent `sbtLoader`, but
22+
* sbt uses its own version of the scala compiler and scala library which
23+
* is not the one we need to run the compiler.
24+
*
25+
* Our solution is to implement a subclass of URLClassLoader with no parent, instead
26+
* we override `loadClass` to load the `xsbti.*` interfaces from `sbtLoader`.
27+
*/
28+
class CompilerClassLoader(urls: Array[URL], sbtLoader: ClassLoader)
29+
extends URLClassLoader(urls, null) {
30+
override def loadClass(className: String, resolve: Boolean): Class[_] =
31+
if (className.startsWith("xsbti.")) {
32+
// We can't use the loadClass overload with two arguments because it's
33+
// protected, but we can do the same by hand (the classloader instance
34+
// from which we call resolveClass does not matter).
35+
val c = sbtLoader.loadClass(className)
36+
if (resolve)
37+
resolveClass(c)
38+
c
39+
} else {
40+
super.loadClass(className, resolve)
41+
}
42+
}
43+
44+
object CompilerClassLoader {
45+
/** Fix the compiler bridge ClassLoader
46+
*
47+
* Soundtrack: https://www.youtube.com/watch?v=imamcajBEJs
48+
*
49+
* The classloader that we get from sbt looks like:
50+
*
51+
* URLClassLoader(bridgeURLs,
52+
* DualLoader(scalaLoader, notXsbtiFilter, sbtLoader, xsbtiFilter))
53+
*
54+
* DualLoader will load the `xsbti.*` interfaces using `sbtLoader` and
55+
* everything else with `scalaLoader`. Once we have loaded the dotty Main
56+
* class using `scalaLoader`, subsequent classes in the dotty compiler will
57+
* also be loaded by `scalaLoader` and _not_ by the DualLoader. But the sbt
58+
* compiler phases are part of dotty and still need access to the `xsbti.*`
59+
* interfaces in `sbtLoader`, therefore DualLoader does not work for us
60+
* (this issue is not present with scalac because the sbt phases are
61+
* currently defined in the compiler bridge itself, not in scalac).
62+
*
63+
* CompilerClassLoader is a replacement for DualLoader. Until we can fix
64+
* this in sbt proper, we need to use reflection to construct our own
65+
* fixed classloader:
66+
*
67+
* URLClassLoader(bridgeURLs,
68+
* CompilerClassLoader(scalaLoader.getURLs, sbtLoader))
69+
*
70+
* @param bridgeLoader The classloader that sbt uses to load the compiler bridge
71+
* @return A fixed classloader that works with dotty
72+
*/
73+
def fixBridgeLoader(bridgeLoader: ClassLoader) = bridgeLoader match {
74+
case bridgeLoader: URLClassLoader =>
75+
val dualLoader = bridgeLoader.getParent
76+
val dualLoaderClass = dualLoader.getClass
77+
78+
// DualLoader#parentA and DualLoader#parentB are private
79+
val parentAField = dualLoaderClass.getDeclaredField("parentA")
80+
parentAField.setAccessible(true)
81+
val parentBField = dualLoaderClass.getDeclaredField("parentB")
82+
parentBField.setAccessible(true)
83+
val scalaLoader = parentAField.get(dualLoader).asInstanceOf[URLClassLoader]
84+
val sbtLoader = parentBField.get(dualLoader).asInstanceOf[URLClassLoader]
85+
86+
val bridgeURLs = bridgeLoader.getURLs
87+
new URLClassLoader(bridgeURLs,
88+
new CompilerClassLoader(scalaLoader.getURLs, sbtLoader))
89+
}
90+
}

bridge/src/main/scala/xsbt/CompilerInterface.scala

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,41 @@
33
*/
44
package xsbt
55

6-
import xsbti.{ AnalysisCallback, Logger, Problem, Reporter, Severity }
6+
import xsbti.{ AnalysisCallback, Logger, Problem, Reporter, Severity, DependencyContext }
7+
import xsbti.api.SourceAPI
78
import xsbti.compile._
89
import Log.debug
910
import java.io.File
1011

12+
import dotty.tools.dotc.core.Contexts.ContextBase
1113
import dotty.tools.dotc.{ Main => DottyMain }
14+
import dotty.tools.dotc.interfaces._
15+
16+
import java.net.URLClassLoader
1217

1318
final class CompilerInterface {
14-
def newCompiler(options: Array[String], output: Output, initialLog: Logger, initialDelegate: Reporter, resident: Boolean): CachedCompiler =
15-
new CachedCompiler0(options, output, resident)
19+
def newCompiler(options: Array[String], output: Output, initialLog: Logger,
20+
initialDelegate: Reporter, resident: Boolean): CachedCompiler = {
21+
// The classloader that sbt uses to load the compiler bridge is broken
22+
// (see CompilerClassLoader#fixBridgeLoader for details). To workaround
23+
// this we construct our own ClassLoader and then run the following code
24+
// with it:
25+
// new CachedCompilerImpl(options, output, resident)
26+
27+
val bridgeLoader = getClass.getClassLoader
28+
val fixedLoader = CompilerClassLoader.fixBridgeLoader(bridgeLoader)
29+
val cciClass = fixedLoader.loadClass("xsbt.CachedCompilerImpl")
30+
cciClass.getConstructors.head
31+
.newInstance(options, output, resident: java.lang.Boolean)
32+
.asInstanceOf[CachedCompiler]
33+
}
1634

17-
def run(sources: Array[File], changes: DependencyChanges, callback: AnalysisCallback, log: Logger, delegate: Reporter, progress: CompileProgress, cached: CachedCompiler): Unit =
35+
def run(sources: Array[File], changes: DependencyChanges, callback: AnalysisCallback, log: Logger,
36+
delegate: Reporter, progress: CompileProgress, cached: CachedCompiler): Unit =
1837
cached.run(sources, changes, callback, log, delegate, progress)
1938
}
2039

21-
private final class CachedCompiler0(args: Array[String], output: Output, resident: Boolean) extends CachedCompiler {
40+
class CachedCompilerImpl(args: Array[String], output: Output, resident: Boolean) extends CachedCompiler {
2241
val outputArgs =
2342
output match {
2443
case multi: MultipleOutput =>
@@ -35,7 +54,11 @@ private final class CachedCompiler0(args: Array[String], output: Output, residen
3554
}
3655
private[this] def run(sources: List[File], changes: DependencyChanges, callback: AnalysisCallback, log: Logger, compileProgress: CompileProgress): Unit = {
3756
debug(log, args.mkString("Calling Dotty compiler with arguments (CompilerInterface):\n\t", "\n\t", ""))
38-
val reporter = DottyMain.process(commandArguments(sources.toArray))
57+
val ctx = (new ContextBase).initialCtx.fresh
58+
.setSbtCallback(callback)
59+
val cl = getClass.getClassLoader.asInstanceOf[URLClassLoader]
60+
61+
val reporter = DottyMain.process(commandArguments(sources.toArray), ctx)
3962
if (reporter.hasErrors) {
4063
throw new InterfaceCompileFailed(args, Array())
4164
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
trait A {
2+
def x: Int
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
trait B extends A {
2+
override def x = 2
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
trait C extends A {
2+
def x = 5
3+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
trait D extends C with B
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
trait C extends A {
2+
abstract override def x = super.x + 5
3+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import sbt._
2+
import Keys._
3+
4+
object DottyInjectedPlugin extends AutoPlugin {
5+
override def requires = plugins.JvmPlugin
6+
override def trigger = allRequirements
7+
8+
override val projectSettings = Seq(
9+
scalaVersion := "0.1-SNAPSHOT",
10+
scalacOptions += "-language:Scala2",
11+
scalaBinaryVersion := "2.11",
12+
autoScalaLibrary := false,
13+
libraryDependencies ++= Seq("org.scala-lang" % "scala-library" % "2.11.5"),
14+
scalaCompilerBridgeSource := ("ch.epfl.lamp" % "dotty-bridge" % "0.1-SNAPSHOT" % "component").sources()
15+
)
16+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
> compile
2+
$ copy-file changes/C2.scala C.scala
3+
-> compile
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
InputKey[Unit]("check-number-of-compiler-iterations") <<= inputTask { (argTask: TaskKey[Seq[String]]) =>
2+
(argTask, compile in Compile) map { (args: Seq[String], a: sbt.inc.Analysis) =>
3+
assert(args.size == 1)
4+
val expectedIterationsNumber = args(0).toInt
5+
assert(a.compilations.allCompilations.size == expectedIterationsNumber, "a.compilations.allCompilations.size = %d (expected %d)".format(a.compilations.allCompilations.size, expectedIterationsNumber))
6+
}
7+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
object Bar {
2+
def bar: Outer.TypeInner = null
3+
// comment to trigger recompilation
4+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import sbt._
2+
import Keys._
3+
4+
object DottyInjectedPlugin extends AutoPlugin {
5+
override def requires = plugins.JvmPlugin
6+
override def trigger = allRequirements
7+
8+
override val projectSettings = Seq(
9+
scalaVersion := "0.1-SNAPSHOT",
10+
scalacOptions += "-language:Scala2",
11+
scalaBinaryVersion := "2.11",
12+
autoScalaLibrary := false,
13+
libraryDependencies ++= Seq("org.scala-lang" % "scala-library" % "2.11.5"),
14+
scalaCompilerBridgeSource := ("ch.epfl.lamp" % "dotty-bridge" % "0.1-SNAPSHOT" % "component").sources()
15+
)
16+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
object Bar {
2+
def bar: Outer.TypeInner = null
3+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
object Outer {
2+
class Inner { type Xyz }
3+
4+
type TypeInner = Inner { type Xyz = Int }
5+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
class Impl {
2+
def bleep = Bar.bar
3+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Test for separate compilation and proper value of
2+
# the OVERRIDE flag when abstract types, type alias
3+
# and structural type are involved
4+
# See https://github.com/sbt/sbt/issues/726 for details
5+
6+
# introduces first compile iteration
7+
> compile
8+
# this change adds a comment and does not change api so introduces
9+
# only one additional compile iteration
10+
$ copy-file changes/Bar1.scala src/main/scala/Bar.scala
11+
# second iteration
12+
#> compile
13+
# check if there are only two compile iterations performed
14+
> check-number-of-compiler-iterations 2
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
trait A {
2+
type S[_]
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
trait B extends A {
2+
type F = S[Int]
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
trait A {
2+
type S
3+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import sbt._
2+
import Keys._
3+
4+
object DottyInjectedPlugin extends AutoPlugin {
5+
override def requires = plugins.JvmPlugin
6+
override def trigger = allRequirements
7+
8+
override val projectSettings = Seq(
9+
scalaVersion := "0.1-SNAPSHOT",
10+
scalacOptions += "-language:Scala2",
11+
scalaBinaryVersion := "2.11",
12+
autoScalaLibrary := false,
13+
libraryDependencies ++= Seq("org.scala-lang" % "scala-library" % "2.11.5"),
14+
scalaCompilerBridgeSource := ("ch.epfl.lamp" % "dotty-bridge" % "0.1-SNAPSHOT" % "component").sources()
15+
)
16+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
> compile
2+
3+
# remove type arguments from S
4+
$ copy-file changes/A.scala A.scala
5+
6+
# Both A.scala and B.scala should be recompiled, producing a compile error
7+
-> compile
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package example
2+
3+
object A
4+
{
5+
val x: Int = 3
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package example
2+
3+
object A
4+
{
5+
val x: Int = B.y
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package example
2+
3+
object A
4+
{
5+
val x: String = B.y
6+
}

0 commit comments

Comments
 (0)