diff options
Diffstat (limited to 'src/compiler')
55 files changed, 2164 insertions, 722 deletions
diff --git a/src/compiler/scala/reflect/macros/contexts/Infrastructure.scala b/src/compiler/scala/reflect/macros/contexts/Infrastructure.scala index df7aa4d2be..7088058145 100644 --- a/src/compiler/scala/reflect/macros/contexts/Infrastructure.scala +++ b/src/compiler/scala/reflect/macros/contexts/Infrastructure.scala @@ -12,5 +12,5 @@ trait Infrastructure { def compilerSettings: List[String] = universe.settings.recreateArgs - def classPath: List[java.net.URL] = global.classPath.asURLs + def classPath: List[java.net.URL] = global.classPath.asURLs.toList } diff --git a/src/compiler/scala/tools/nsc/ClassPathMemoryConsumptionTester.scala b/src/compiler/scala/tools/nsc/ClassPathMemoryConsumptionTester.scala new file mode 100644 index 0000000000..2faf6c6272 --- /dev/null +++ b/src/compiler/scala/tools/nsc/ClassPathMemoryConsumptionTester.scala @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2014 Contributor. All rights reserved. + */ +package scala.tools.nsc + +import scala.io.StdIn.readLine + +/** + * Simple application to check out amount of memory used by chosen classpath representation. + * It allows us to create many scalac-like calls based on specified parameters, where each main retains Global. + * And we need additional tool (e.g. profiler) to measure memory consumption itself. + */ +object ClassPathMemoryConsumptionTester { + + private class TestSettings extends Settings { + val requiredInstances = IntSetting("-requiredInstances", + "Determine how many times classpath should be loaded", 10, Some((1, 10000)), (_: String) => None) + } + + private class MainRetainsGlobal extends scala.tools.nsc.MainClass { + var retainedGlobal: Global = _ + override def doCompile(compiler: Global) { + retainedGlobal = compiler + super.doCompile(compiler) + } + } + + def main(args: Array[String]): Unit = { + if (args contains "-help") usage() + else doTest(args) + } + + private def doTest(args: Array[String]) = { + val settings = loadSettings(args.toList) + + val mains = (1 to settings.requiredInstances.value) map (_ => new MainRetainsGlobal) + + // we need original settings without additional params to be able to use them later + val baseArgs = argsWithoutRequiredInstances(args) + + println(s"Loading classpath ${settings.requiredInstances.value} times") + val startTime = System.currentTimeMillis() + + mains map (_.process(baseArgs)) + + val elapsed = System.currentTimeMillis() - startTime + println(s"Operation finished - elapsed $elapsed ms") + println("Memory consumption can be now measured") + + var textFromStdIn = "" + while (textFromStdIn.toLowerCase != "exit") + textFromStdIn = readLine("Type 'exit' to close application: ") + } + + /** + * Prints usage information + */ + private def usage(): Unit = + println( """Use classpath and sourcepath options like in the case of e.g. 'scala' command. + | There's also one additional option: + | -requiredInstances <int value> Determine how many times classpath should be loaded + """.stripMargin.trim) + + private def loadSettings(args: List[String]) = { + val settings = new TestSettings() + settings.processArguments(args, processAll = true) + if (settings.classpath.isDefault) + settings.classpath.value = sys.props("java.class.path") + settings + } + + private def argsWithoutRequiredInstances(args: Array[String]) = { + val instancesIndex = args.indexOf("-requiredInstances") + if (instancesIndex == -1) args + else args.dropRight(args.length - instancesIndex) ++ args.drop(instancesIndex + 2) + } +} diff --git a/src/compiler/scala/tools/nsc/CompileClient.scala b/src/compiler/scala/tools/nsc/CompileClient.scala index 3017d8c9cc..f259504473 100644 --- a/src/compiler/scala/tools/nsc/CompileClient.scala +++ b/src/compiler/scala/tools/nsc/CompileClient.scala @@ -43,8 +43,8 @@ class StandardCompileClient extends HasCompileSocket with CompileOutputCommon { info(vmArgs.mkString("[VM arguments: ", " ", "]")) val socket = - if (settings.server.value == "") compileSocket.getOrCreateSocket(vmArgs mkString " ", !shutdown) - else Some(compileSocket.getSocket(settings.server.value)) + if (settings.server.value == "") compileSocket.getOrCreateSocket(vmArgs mkString " ", !shutdown, settings.port.value) + else compileSocket.getSocket(settings.server.value) socket match { case Some(sock) => compileOnServer(sock, fscArgs) diff --git a/src/compiler/scala/tools/nsc/CompileServer.scala b/src/compiler/scala/tools/nsc/CompileServer.scala index 029e1c4629..aa02957a6c 100644 --- a/src/compiler/scala/tools/nsc/CompileServer.scala +++ b/src/compiler/scala/tools/nsc/CompileServer.scala @@ -6,6 +6,7 @@ package scala.tools.nsc import java.io.PrintStream +import io.Directory import scala.tools.nsc.reporters.{Reporter, ConsoleReporter} import scala.reflect.internal.util.{FakePos, Position} import scala.tools.util.SocketServer @@ -19,7 +20,7 @@ import settings.FscSettings * @author Martin Odersky * @version 1.0 */ -class StandardCompileServer extends SocketServer { +class StandardCompileServer(fixPort: Int = 0) extends SocketServer(fixPort) { lazy val compileSocket: CompileSocket = CompileSocket private var compiler: Global = null @@ -166,12 +167,12 @@ class StandardCompileServer extends SocketServer { } -object CompileServer extends StandardCompileServer { +object CompileServer { /** A directory holding redirected output */ - private lazy val redirectDir = (compileSocket.tmpDir / "output-redirects").createDirectory() + //private lazy val redirectDir = (compileSocket.tmpDir / "output-redirects").createDirectory() - private def createRedirect(filename: String) = - new PrintStream((redirectDir / filename).createFile().bufferedOutput()) + private def createRedirect(dir: Directory, filename: String) = + new PrintStream((dir / filename).createFile().bufferedOutput()) def main(args: Array[String]) = execute(() => (), args) @@ -187,21 +188,33 @@ object CompileServer extends StandardCompileServer { */ def execute(startupCallback : () => Unit, args: Array[String]) { val debug = args contains "-v" + var port = 0 + val i = args.indexOf("-p") + if (i >= 0 && args.length > i + 1) { + scala.util.control.Exception.ignoring(classOf[NumberFormatException]) { + port = args(i + 1).toInt + } + } + + // Create instance rather than extend to pass a port parameter. + val server = new StandardCompileServer(port) + val redirectDir = (server.compileSocket.tmpDir / "output-redirects").createDirectory() + if (debug) { - echo("Starting CompileServer on port " + port) - echo("Redirect dir is " + redirectDir) + server.echo("Starting CompileServer on port " + server.port) + server.echo("Redirect dir is " + redirectDir) } - Console.withErr(createRedirect("scala-compile-server-err.log")) { - Console.withOut(createRedirect("scala-compile-server-out.log")) { - Console.err.println("...starting server on socket "+port+"...") + Console.withErr(createRedirect(redirectDir, "scala-compile-server-err.log")) { + Console.withOut(createRedirect(redirectDir, "scala-compile-server-out.log")) { + Console.err.println("...starting server on socket "+server.port+"...") Console.err.flush() - compileSocket setPort port + server.compileSocket setPort server.port startupCallback() - run() + server.run() - compileSocket deletePort port + server.compileSocket deletePort server.port } } } diff --git a/src/compiler/scala/tools/nsc/CompileSocket.scala b/src/compiler/scala/tools/nsc/CompileSocket.scala index c693fbe8e2..27a14141fa 100644 --- a/src/compiler/scala/tools/nsc/CompileSocket.scala +++ b/src/compiler/scala/tools/nsc/CompileSocket.scala @@ -46,6 +46,9 @@ trait HasCompileSocket { class CompileSocket extends CompileOutputCommon { protected lazy val compileClient: StandardCompileClient = CompileClient def verbose = compileClient.verbose + + /* Fixes the port where to start the server, 0 yields some free port */ + var fixPort = 0 /** The prefix of the port identification file, which is followed * by the port number. @@ -64,7 +67,7 @@ class CompileSocket extends CompileOutputCommon { /** The class name of the scala compile server */ protected val serverClass = "scala.tools.nsc.CompileServer" - protected def serverClassArgs = if (verbose) List("-v") else Nil // debug + protected def serverClassArgs = (if (verbose) List("-v") else Nil) ::: (if (fixPort > 0) List("-p", fixPort.toString) else Nil) /** A temporary directory to use */ val tmpDir = { @@ -104,9 +107,14 @@ class CompileSocket extends CompileOutputCommon { def portFile(port: Int) = portsDir / File(port.toString) /** Poll for a server port number; return -1 if none exists yet */ - private def pollPort(): Int = portsDir.list.toList match { + private def pollPort(): Int = if (fixPort > 0) { + if (portsDir.list.toList.exists(_.name == fixPort.toString)) fixPort else -1 + } else portsDir.list.toList match { case Nil => -1 - case x :: xs => try x.name.toInt finally xs foreach (_.delete()) + case x :: xs => try x.name.toInt catch { + case e: Exception => x.delete() + throw e + } } /** Get the port number to which a scala compile server is connected; @@ -152,7 +160,8 @@ class CompileSocket extends CompileOutputCommon { * create a new daemon if necessary. Returns None if the connection * cannot be established. */ - def getOrCreateSocket(vmArgs: String, create: Boolean = true): Option[Socket] = { + def getOrCreateSocket(vmArgs: String, create: Boolean = true, fixedPort: Int = 0): Option[Socket] = { + fixPort = fixedPort val maxMillis = 10L * 1000 // try for 10 seconds val retryDelay = 50L val maxAttempts = (maxMillis / retryDelay).toInt @@ -186,14 +195,17 @@ class CompileSocket extends CompileOutputCommon { try { Some(x.toInt) } catch { case _: NumberFormatException => None } - def getSocket(serverAdr: String): Socket = ( - for ((name, portStr) <- splitWhere(serverAdr, _ == ':', doDropIndex = true) ; port <- parseInt(portStr)) yield + def getSocket(serverAdr: String): Option[Socket] = ( + for ((name, portStr) <- splitWhere(serverAdr, _ == ':', doDropIndex = true) ; port <- parseInt(portStr)) yield getSocket(name, port) ) getOrElse fatal("Malformed server address: %s; exiting" format serverAdr) - def getSocket(hostName: String, port: Int): Socket = - Socket(hostName, port).opt getOrElse fatal("Unable to establish connection to server %s:%d; exiting".format(hostName, port)) - + def getSocket(hostName: String, port: Int): Option[Socket] = { + val sock = Socket(hostName, port).opt + if (sock.isEmpty) warn("Unable to establish connection to server %s:%d".format(hostName, port)) + sock + } + def getPassword(port: Int): String = { val ff = portFile(port) val f = ff.bufferedReader() diff --git a/src/compiler/scala/tools/nsc/GenericRunnerSettings.scala b/src/compiler/scala/tools/nsc/GenericRunnerSettings.scala index ad75d02bff..1289d55c37 100644 --- a/src/compiler/scala/tools/nsc/GenericRunnerSettings.scala +++ b/src/compiler/scala/tools/nsc/GenericRunnerSettings.scala @@ -5,10 +5,11 @@ package scala.tools.nsc -import scala.tools.util.PathResolver +import java.net.URL +import scala.tools.util.PathResolverFactory class GenericRunnerSettings(error: String => Unit) extends Settings(error) { - def classpathURLs = new PathResolver(this).asURLs + def classpathURLs: Seq[URL] = PathResolverFactory.create(this).resultAsURLs val howtorun = ChoiceSetting( diff --git a/src/compiler/scala/tools/nsc/Global.scala b/src/compiler/scala/tools/nsc/Global.scala index 430424d0f8..733664c30a 100644 --- a/src/compiler/scala/tools/nsc/Global.scala +++ b/src/compiler/scala/tools/nsc/Global.scala @@ -14,11 +14,10 @@ import scala.compat.Platform.currentTime import scala.collection.{ mutable, immutable } import io.{ SourceReader, AbstractFile, Path } import reporters.{ Reporter, ConsoleReporter } -import util.{ ClassPath, MergedClassPath, StatisticsInfo, returning, stackTraceString } +import util.{ ClassFileLookup, ClassPath, MergedClassPath, StatisticsInfo, returning } import scala.reflect.ClassTag -import scala.reflect.internal.util.{ OffsetPosition, SourceFile, NoSourceFile, BatchSourceFile, ScriptSourceFile } -import scala.reflect.internal.pickling.{ PickleBuffer, PickleFormat } -import scala.reflect.io.VirtualFile +import scala.reflect.internal.util.{ SourceFile, NoSourceFile, BatchSourceFile, ScriptSourceFile } +import scala.reflect.internal.pickling.PickleBuffer import symtab.{ Flags, SymbolTable, SymbolTrackers } import symtab.classfile.Pickler import plugins.Plugins @@ -35,6 +34,8 @@ import backend.opt.{ Inliners, InlineExceptionHandlers, ConstantOptimization, Cl import backend.icode.analysis._ import scala.language.postfixOps import scala.tools.nsc.ast.{TreeGen => AstTreeGen} +import scala.tools.nsc.classpath.FlatClassPath +import scala.tools.nsc.settings.ClassPathRepresentationType class Global(var currentSettings: Settings, var reporter: Reporter) extends SymbolTable @@ -58,7 +59,12 @@ class Global(var currentSettings: Settings, var reporter: Reporter) class GlobalMirror extends Roots(NoSymbol) { val universe: self.type = self - def rootLoader: LazyType = new loaders.PackageLoader(classPath) + def rootLoader: LazyType = { + settings.YclasspathImpl.value match { + case ClassPathRepresentationType.Flat => new loaders.PackageLoaderUsingFlatClassPath(FlatClassPath.RootPackage, flatClassPath) + case ClassPathRepresentationType.Recursive => new loaders.PackageLoader(recursiveClassPath) + } + } override def toString = "compiler mirror" } implicit val MirrorTag: ClassTag[Mirror] = ClassTag[Mirror](classOf[GlobalMirror]) @@ -104,7 +110,14 @@ class Global(var currentSettings: Settings, var reporter: Reporter) type PlatformClassPath = ClassPath[AbstractFile] type OptClassPath = Option[PlatformClassPath] - def classPath: PlatformClassPath = platform.classPath + def classPath: ClassFileLookup[AbstractFile] = settings.YclasspathImpl.value match { + case ClassPathRepresentationType.Flat => flatClassPath + case ClassPathRepresentationType.Recursive => recursiveClassPath + } + + private def recursiveClassPath: ClassPath[AbstractFile] = platform.classPath + + private def flatClassPath: FlatClassPath = platform.flatClassPath // sub-components -------------------------------------------------- @@ -319,7 +332,7 @@ class Global(var currentSettings: Settings, var reporter: Reporter) None } - val charset = ( if (settings.encoding.isSetByUser) Some(settings.encoding.value) else None ) flatMap loadCharset getOrElse { + val charset = settings.encoding.valueSetByUser flatMap loadCharset getOrElse { settings.encoding.value = defaultEncoding // A mandatory charset Charset.forName(defaultEncoding) } @@ -334,16 +347,16 @@ class Global(var currentSettings: Settings, var reporter: Reporter) } } - ( if (settings.sourceReader.isSetByUser) Some(settings.sourceReader.value) else None ) flatMap loadReader getOrElse { + settings.sourceReader.valueSetByUser flatMap loadReader getOrElse { new SourceReader(charset.newDecoder(), reporter) } } - if (settings.verbose || settings.Ylogcp) { + if (settings.verbose || settings.Ylogcp) reporter.echo( - s"[search path for source files: ${classPath.sourcepaths.mkString(",")}]\n"+ - s"[search path for class files: ${classPath.asClasspathString}") - } + s"[search path for source files: ${classPath.asSourcePathString}]\n" + + s"[search path for class files: ${classPath.asClassPathString}]" + ) // The current division between scala.reflect.* and scala.tools.nsc.* is pretty // clunky. It is often difficult to have a setting influence something without having @@ -846,6 +859,9 @@ class Global(var currentSettings: Settings, var reporter: Reporter) /** Extend classpath of `platform` and rescan updated packages. */ def extendCompilerClassPath(urls: URL*): Unit = { + if (settings.YclasspathImpl.value == ClassPathRepresentationType.Flat) + throw new UnsupportedOperationException("Flat classpath doesn't support extending the compiler classpath") + val newClassPath = platform.classPath.mergeUrlsIntoClassPath(urls: _*) platform.currentClassPath = Some(newClassPath) // Reload all specified jars into this compiler instance @@ -881,8 +897,11 @@ class Global(var currentSettings: Settings, var reporter: Reporter) * entries on the classpath. */ def invalidateClassPathEntries(paths: String*): Unit = { + if (settings.YclasspathImpl.value == ClassPathRepresentationType.Flat) + throw new UnsupportedOperationException("Flat classpath doesn't support the classpath invalidation") + implicit object ClassPathOrdering extends Ordering[PlatformClassPath] { - def compare(a:PlatformClassPath, b:PlatformClassPath) = a.asClasspathString compare b.asClasspathString + def compare(a:PlatformClassPath, b:PlatformClassPath) = a.asClassPathString compare b.asClassPathString } val invalidated, failed = new mutable.ListBuffer[ClassSymbol] classPath match { @@ -910,10 +929,10 @@ class Global(var currentSettings: Settings, var reporter: Reporter) informProgress(s"classpath updated on entries [${subst.keys mkString ","}]") def mkClassPath(elems: Iterable[PlatformClassPath]): PlatformClassPath = if (elems.size == 1) elems.head - else new MergedClassPath(elems, classPath.context) + else new MergedClassPath(elems, recursiveClassPath.context) val oldEntries = mkClassPath(subst.keys) val newEntries = mkClassPath(subst.values) - mergeNewEntries(newEntries, RootClass, Some(classPath), Some(oldEntries), invalidated, failed) + mergeNewEntries(newEntries, RootClass, Some(recursiveClassPath), Some(oldEntries), invalidated, failed) } } def show(msg: String, syms: scala.collection.Traversable[Symbol]) = @@ -1590,10 +1609,9 @@ class Global(var currentSettings: Settings, var reporter: Reporter) } } - /** Reset package class to state at typer (not sure what this - * is needed for?) + /** Reset package class to state at typer (not sure what this is needed for?) */ - private def resetPackageClass(pclazz: Symbol) { + private def resetPackageClass(pclazz: Symbol): Unit = if (typerPhase != NoPhase) { enteringPhase(firstPhase) { pclazz.setInfo(enteringPhase(typerPhase)(pclazz.info)) } diff --git a/src/compiler/scala/tools/nsc/ObjectRunner.scala b/src/compiler/scala/tools/nsc/ObjectRunner.scala index 95264aeda6..7c14f4943f 100644 --- a/src/compiler/scala/tools/nsc/ObjectRunner.scala +++ b/src/compiler/scala/tools/nsc/ObjectRunner.scala @@ -18,14 +18,14 @@ trait CommonRunner { * @throws NoSuchMethodException * @throws InvocationTargetException */ - def run(urls: List[URL], objectName: String, arguments: Seq[String]) { + def run(urls: Seq[URL], objectName: String, arguments: Seq[String]) { (ScalaClassLoader fromURLs urls).run(objectName, arguments) } /** Catches exceptions enumerated by run (in the case of InvocationTargetException, * unwrapping it) and returns it any thrown in Left(x). */ - def runAndCatch(urls: List[URL], objectName: String, arguments: Seq[String]): Either[Throwable, Boolean] = { + def runAndCatch(urls: Seq[URL], objectName: String, arguments: Seq[String]): Either[Throwable, Boolean] = { try { run(urls, objectName, arguments) ; Right(true) } catch { case e: Throwable => Left(unwrap(e)) } } diff --git a/src/compiler/scala/tools/nsc/PhaseAssembly.scala b/src/compiler/scala/tools/nsc/PhaseAssembly.scala index cfb4cd23a1..1eb6c9da2c 100644 --- a/src/compiler/scala/tools/nsc/PhaseAssembly.scala +++ b/src/compiler/scala/tools/nsc/PhaseAssembly.scala @@ -199,7 +199,7 @@ trait PhaseAssembly { // Add all phases in the set to the graph val graph = phasesSetToDepGraph(phasesSet) - val dot = if (settings.genPhaseGraph.isSetByUser) Some(settings.genPhaseGraph.value) else None + val dot = settings.genPhaseGraph.valueSetByUser // Output the phase dependency graph at this stage def dump(stage: Int) = dot foreach (n => graphToDotFile(graph, s"$n-$stage.dot")) diff --git a/src/compiler/scala/tools/nsc/ScriptRunner.scala b/src/compiler/scala/tools/nsc/ScriptRunner.scala index 7d5c6f6fff..6d24b31531 100644 --- a/src/compiler/scala/tools/nsc/ScriptRunner.scala +++ b/src/compiler/scala/tools/nsc/ScriptRunner.scala @@ -8,7 +8,10 @@ package tools.nsc import io.{ AbstractFile, Directory, File, Path } import java.io.IOException +import scala.tools.nsc.classpath.DirectoryFlatClassPath import scala.tools.nsc.reporters.{Reporter,ConsoleReporter} +import scala.tools.nsc.settings.ClassPathRepresentationType +import scala.tools.nsc.util.ClassPath.DefaultJavaContext import util.Exceptional.unwrap /** An object that runs Scala code in script files. @@ -112,8 +115,10 @@ class ScriptRunner extends HasCompileSocket { } def hasClassToRun(d: Directory): Boolean = { - import util.ClassPath.{ DefaultJavaContext => ctx } - val cp = ctx.newClassPath(AbstractFile.getDirectory(d)) + val cp = settings.YclasspathImpl.value match { + case ClassPathRepresentationType.Recursive => DefaultJavaContext.newClassPath(AbstractFile.getDirectory(d)) + case ClassPathRepresentationType.Flat => DirectoryFlatClassPath(d.jfile) + } cp.findClass(mainClass).isDefined } diff --git a/src/compiler/scala/tools/nsc/ast/parser/MarkupParsers.scala b/src/compiler/scala/tools/nsc/ast/parser/MarkupParsers.scala index d3f495f280..f1517e56a0 100755 --- a/src/compiler/scala/tools/nsc/ast/parser/MarkupParsers.scala +++ b/src/compiler/scala/tools/nsc/ast/parser/MarkupParsers.scala @@ -346,12 +346,11 @@ trait MarkupParsers { // parse more XML ? if (charComingAfter(xSpaceOpt()) == '<') { - xSpaceOpt() - while (ch == '<') { + do { + xSpaceOpt() nextch() ts append element - xSpaceOpt() - } + } while (charComingAfter(xSpaceOpt()) == '<') handle.makeXMLseq(r2p(start, start, curOffset), ts) } else { diff --git a/src/compiler/scala/tools/nsc/ast/parser/Scanners.scala b/src/compiler/scala/tools/nsc/ast/parser/Scanners.scala index 9ebc94b5fc..92833d647b 100644 --- a/src/compiler/scala/tools/nsc/ast/parser/Scanners.scala +++ b/src/compiler/scala/tools/nsc/ast/parser/Scanners.scala @@ -453,18 +453,15 @@ trait Scanners extends ScannersCommon { getOperatorRest() } case '0' => - def fetchZero() = { - putChar(ch) + def fetchLeadingZero(): Unit = { nextChar() - if (ch == 'x' || ch == 'X') { - nextChar() - base = 16 - } else { - base = 8 + ch match { + case 'x' | 'X' => base = 16 ; nextChar() + case _ => base = 8 // single decimal zero, perhaps } - getNumber() } - fetchZero() + fetchLeadingZero() + getNumber() case '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' => base = 10 getNumber() @@ -902,62 +899,61 @@ trait Scanners extends ScannersCommon { */ def charVal: Char = if (strVal.length > 0) strVal.charAt(0) else 0 - /** Convert current strVal, base to long value + /** Convert current strVal, base to long value. * This is tricky because of max negative value. + * + * Conversions in base 10 and 16 are supported. As a permanent migration + * path, attempts to write base 8 literals except `0` emit a verbose error. */ def intVal(negated: Boolean): Long = { - if (token == CHARLIT && !negated) { - charVal.toLong - } else { - var value: Long = 0 - val divider = if (base == 10) 1 else 2 - val limit: Long = - if (token == LONGLIT) Long.MaxValue else Int.MaxValue - var i = 0 + def malformed: Long = { + if (base == 8) syntaxError("Decimal integer literals may not have a leading zero. (Octal syntax is obsolete.)") + else syntaxError("malformed integer number") + 0 + } + def tooBig: Long = { + syntaxError("integer number too large") + 0 + } + def intConvert: Long = { val len = strVal.length - while (i < len) { - val d = digit2int(strVal charAt i, base) - if (d < 0) { - syntaxError("malformed integer number") - return 0 - } - if (value < 0 || - limit / (base / divider) < value || - limit - (d / divider) < value * (base / divider) && - !(negated && limit == value * base - 1 + d)) { - syntaxError("integer number too large") - return 0 - } - value = value * base + d - i += 1 + if (len == 0) { + if (base != 8) syntaxError("missing integer number") // e.g., 0x; + 0 + } else { + val divider = if (base == 10) 1 else 2 + val limit: Long = if (token == LONGLIT) Long.MaxValue else Int.MaxValue + @tailrec def convert(value: Long, i: Int): Long = + if (i >= len) value + else { + val d = digit2int(strVal charAt i, base) + if (d < 0) + malformed + else if (value < 0 || + limit / (base / divider) < value || + limit - (d / divider) < value * (base / divider) && + !(negated && limit == value * base - 1 + d)) + tooBig + else + convert(value * base + d, i + 1) + } + val result = convert(0, 0) + if (base == 8) malformed else if (negated) -result else result } - if (negated) -value else value } + if (token == CHARLIT && !negated) charVal.toLong else intConvert } def intVal: Long = intVal(negated = false) /** Convert current strVal, base to double value - */ + */ def floatVal(negated: Boolean): Double = { - - val limit: Double = - if (token == DOUBLELIT) Double.MaxValue else Float.MaxValue + val limit: Double = if (token == DOUBLELIT) Double.MaxValue else Float.MaxValue try { val value: Double = java.lang.Double.valueOf(strVal).doubleValue() - def isDeprecatedForm = { - val idx = strVal indexOf '.' - (idx == strVal.length - 1) || ( - (idx >= 0) - && (idx + 1 < strVal.length) - && (!Character.isDigit(strVal charAt (idx + 1))) - ) - } if (value > limit) syntaxError("floating point number too large") - if (isDeprecatedForm) - syntaxError("floating point number is missing digit after dot") - if (negated) -value else value } catch { case _: NumberFormatException => @@ -968,86 +964,44 @@ trait Scanners extends ScannersCommon { def floatVal: Double = floatVal(negated = false) - def checkNoLetter(): Unit = { + def checkNoLetter(): Unit = { if (isIdentifierPart(ch) && ch >= ' ') syntaxError("Invalid literal number") } - /** Read a number into strVal and set base */ - protected def getNumber(): Unit = { - val base1 = if (base < 10) 10 else base - // Read 8,9's even if format is octal, produce a malformed number error afterwards. - // At this point, we have already read the first digit, so to tell an innocent 0 apart - // from an octal literal 0123... (which we want to disallow), we check whether there - // are any additional digits coming after the first one we have already read. - var notSingleZero = false - while (digit2int(ch, base1) >= 0) { - putChar(ch) - nextChar() - notSingleZero = true - } - token = INTLIT - - /* When we know for certain it's a number after using a touch of lookahead */ - def restOfNumber() = { - putChar(ch) - nextChar() + /** Read a number into strVal. + * + * The `base` can be 8, 10 or 16, where base 8 flags a leading zero. + * For ints, base 8 is legal only for the case of exactly one zero. + */ + protected def getNumber(): Unit = { + // consume digits of a radix + def consumeDigits(radix: Int): Unit = + while (digit2int(ch, radix) >= 0) { + putChar(ch) + nextChar() + } + // adding decimal point is always OK because `Double valueOf "0."` is OK + def restOfNonIntegralNumber(): Unit = { + putChar('.') + if (ch == '.') nextChar() getFraction() } - def restOfUncertainToken() = { - def isEfd = ch match { case 'e' | 'E' | 'f' | 'F' | 'd' | 'D' => true ; case _ => false } - def isL = ch match { case 'l' | 'L' => true ; case _ => false } - - if (base <= 10 && isEfd) - getFraction() - else { - // Checking for base == 8 is not enough, because base = 8 is set - // as soon as a 0 is read in `case '0'` of method fetchToken. - if (base == 8 && notSingleZero) syntaxError("Non-zero integral values may not have a leading zero.") - setStrVal() - if (isL) { - nextChar() - token = LONGLIT - } - else checkNoLetter() + // after int: 5e7f, 42L, 42.toDouble but not 42b. Repair 0d. + def restOfNumber(): Unit = { + ch match { + case 'e' | 'E' | 'f' | 'F' | + 'd' | 'D' => if (cbuf.isEmpty) putChar('0'); restOfNonIntegralNumber() + case 'l' | 'L' => token = LONGLIT ; setStrVal() ; nextChar() + case _ => token = INTLIT ; setStrVal() ; checkNoLetter() } } - if (base > 10 || ch != '.') - restOfUncertainToken() - else { - val lookahead = lookaheadReader - val c = lookahead.getc() - - /* Prohibit 1. */ - if (!isDigit(c)) - return setStrVal() - - val isDefinitelyNumber = (c: @switch) match { - /** Another digit is a giveaway. */ - case '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' => - true + // consume leading digits, provisionally an Int + consumeDigits(if (base == 16) 16 else 10) - /* Backquoted idents like 22.`foo`. */ - case '`' => - return setStrVal() /** Note the early return */ - - /* These letters may be part of a literal, or a method invocation on an Int. - */ - case 'd' | 'D' | 'f' | 'F' => - !isIdentifierPart(lookahead.getc()) - - /* A little more special handling for e.g. 5e7 */ - case 'e' | 'E' => - val ch = lookahead.getc() - !isIdentifierPart(ch) || (isDigit(ch) || ch == '+' || ch == '-') - - case x => - !isIdentifierStart(x) - } - if (isDefinitelyNumber) restOfNumber() - else restOfUncertainToken() - } + val detectedFloat: Boolean = base != 16 && ch == '.' && isDigit(lookaheadReader.getc) + if (detectedFloat) restOfNonIntegralNumber() else restOfNumber() } /** Parse character literal if current character is followed by \', diff --git a/src/compiler/scala/tools/nsc/backend/JavaPlatform.scala b/src/compiler/scala/tools/nsc/backend/JavaPlatform.scala index 4877bd9b80..6bd123c51f 100644 --- a/src/compiler/scala/tools/nsc/backend/JavaPlatform.scala +++ b/src/compiler/scala/tools/nsc/backend/JavaPlatform.scala @@ -7,7 +7,10 @@ package scala.tools.nsc package backend import io.AbstractFile -import util.{ClassPath,MergedClassPath,DeltaClassPath} +import scala.tools.nsc.classpath.FlatClassPath +import scala.tools.nsc.settings.ClassPathRepresentationType +import scala.tools.nsc.util.{ ClassPath, DeltaClassPath, MergedClassPath } +import scala.tools.util.FlatClassPathResolver import scala.tools.util.PathResolver trait JavaPlatform extends Platform { @@ -19,10 +22,20 @@ trait JavaPlatform extends Platform { private[nsc] var currentClassPath: Option[MergedClassPath[AbstractFile]] = None def classPath: ClassPath[AbstractFile] = { + assert(settings.YclasspathImpl.value == ClassPathRepresentationType.Recursive, + "To use recursive classpath representation you must enable it with -YclasspathImpl:recursive compiler option.") + if (currentClassPath.isEmpty) currentClassPath = Some(new PathResolver(settings).result) currentClassPath.get } + private[nsc] lazy val flatClassPath: FlatClassPath = { + assert(settings.YclasspathImpl.value == ClassPathRepresentationType.Flat, + "To use flat classpath representation you must enable it with -YclasspathImpl:flat compiler option.") + + new FlatClassPathResolver(settings).result + } + /** Update classpath with a substituted subentry */ def updateClassPath(subst: Map[ClassPath[AbstractFile], ClassPath[AbstractFile]]) = currentClassPath = Some(new DeltaClassPath(currentClassPath.get, subst)) diff --git a/src/compiler/scala/tools/nsc/backend/Platform.scala b/src/compiler/scala/tools/nsc/backend/Platform.scala index 439cc1efb8..c3bc213be1 100644 --- a/src/compiler/scala/tools/nsc/backend/Platform.scala +++ b/src/compiler/scala/tools/nsc/backend/Platform.scala @@ -8,6 +8,7 @@ package backend import util.ClassPath import io.AbstractFile +import scala.tools.nsc.classpath.FlatClassPath /** The platform dependent pieces of Global. */ @@ -15,9 +16,12 @@ trait Platform { val symbolTable: symtab.SymbolTable import symbolTable._ - /** The compiler classpath. */ + /** The old, recursive implementation of compiler classpath. */ def classPath: ClassPath[AbstractFile] + /** The new implementation of compiler classpath. */ + private[nsc] def flatClassPath: FlatClassPath + /** Update classpath with a substitution that maps entries to entries */ def updateClassPath(subst: Map[ClassPath[AbstractFile], ClassPath[AbstractFile]]) diff --git a/src/compiler/scala/tools/nsc/backend/icode/GenICode.scala b/src/compiler/scala/tools/nsc/backend/icode/GenICode.scala index d9f56b47fa..06623b39cd 100644 --- a/src/compiler/scala/tools/nsc/backend/icode/GenICode.scala +++ b/src/compiler/scala/tools/nsc/backend/icode/GenICode.scala @@ -1495,7 +1495,7 @@ abstract class GenICode extends SubComponent { if (!settings.optimise) { if (l.tpe <:< BoxedNumberClass.tpe) { if (r.tpe <:< BoxedNumberClass.tpe) platform.externalEqualsNumNum - else if (r.tpe <:< BoxedCharacterClass.tpe) platform.externalEqualsNumChar + else if (r.tpe <:< BoxedCharacterClass.tpe) platform.externalEqualsNumObject // will be externalEqualsNumChar in 2.12, SI-9030 else platform.externalEqualsNumObject } else platform.externalEquals } else { diff --git a/src/compiler/scala/tools/nsc/backend/icode/ICodes.scala b/src/compiler/scala/tools/nsc/backend/icode/ICodes.scala index bc35a9e7de..10f0c6ee00 100644 --- a/src/compiler/scala/tools/nsc/backend/icode/ICodes.scala +++ b/src/compiler/scala/tools/nsc/backend/icode/ICodes.scala @@ -113,7 +113,8 @@ abstract class ICodes extends AnyRef global.loaders.lookupMemberAtTyperPhaseIfPossible(sym, name) lazy val symbolTable: global.type = global lazy val loaders: global.loaders.type = global.loaders - def classPath: util.ClassPath[AbstractFile] = ICodes.this.global.platform.classPath + + def classFileLookup: util.ClassFileLookup[AbstractFile] = global.classPath } /** A phase which works on icode. */ diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BCodeAsmCommon.scala b/src/compiler/scala/tools/nsc/backend/jvm/BCodeAsmCommon.scala index 328ec8a033..a5f33aa786 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeAsmCommon.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeAsmCommon.scala @@ -162,4 +162,32 @@ final class BCodeAsmCommon[G <: Global](val global: G) { assoc.collectFirst { case (`nme`.value, LiteralAnnotArg(Constant(value: Symbol))) => value }).flatten.getOrElse(AnnotationRetentionPolicyClassValue) + + def implementedInterfaces(classSym: Symbol): List[Symbol] = { + // Additional interface parents based on annotations and other cues + def newParentForAnnotation(ann: AnnotationInfo): Option[Type] = ann.symbol match { + case RemoteAttr => Some(RemoteInterfaceClass.tpe) + case _ => None + } + + def isInterfaceOrTrait(sym: Symbol) = sym.isInterface || sym.isTrait + + val allParents = classSym.info.parents ++ classSym.annotations.flatMap(newParentForAnnotation) + + // We keep the superClass when computing minimizeParents to eliminate more interfaces. + // Example: T can be eliminated from D + // trait T + // class C extends T + // class D extends C with T + val interfaces = erasure.minimizeParents(allParents) match { + case superClass :: ifs if !isInterfaceOrTrait(superClass.typeSymbol) => + ifs + case ifs => + // minimizeParents removes the superclass if it's redundant, for example: + // trait A + // class C extends Object with A // minimizeParents removes Object + ifs + } + interfaces.map(_.typeSymbol) + } } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala b/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala index daf36ce374..062daa4eac 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala @@ -1225,7 +1225,7 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder { val equalsMethod: Symbol = { if (l.tpe <:< BoxedNumberClass.tpe) { if (r.tpe <:< BoxedNumberClass.tpe) platform.externalEqualsNumNum - else if (r.tpe <:< BoxedCharacterClass.tpe) platform.externalEqualsNumChar + else if (r.tpe <:< BoxedCharacterClass.tpe) platform.externalEqualsNumObject // will be externalEqualsNumChar in 2.12, SI-9030 else platform.externalEqualsNumObject } else platform.externalEquals } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala b/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala index 3b7cbd6392..2238221c83 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala @@ -114,7 +114,7 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { val superClass = if (superClassSym == NoSymbol) None else Some(classBTypeFromSymbol(superClassSym)) - val interfaces = getSuperInterfaces(classSym).map(classBTypeFromSymbol) + val interfaces = implementedInterfaces(classSym).map(classBTypeFromSymbol) val flags = javaFlags(classSym) @@ -182,28 +182,6 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { classBType } - /** - * All interfaces implemented by a class, except for those inherited through the superclass. - * - * TODO @lry share code with GenASM - */ - private def getSuperInterfaces(classSym: Symbol): List[Symbol] = { - - // Additional interface parents based on annotations and other cues - def newParentForAnnotation(ann: AnnotationInfo): Symbol = ann.symbol match { - case RemoteAttr => RemoteInterfaceClass - case _ => NoSymbol - } - - val superInterfaces0: List[Symbol] = classSym.mixinClasses - val superInterfaces = existingSymbols(superInterfaces0 ++ classSym.annotations.map(newParentForAnnotation)).distinct - - assert(!superInterfaces.contains(NoSymbol), s"found NoSymbol among: ${superInterfaces.mkString(", ")}") - assert(superInterfaces.forall(s => s.isInterface || s.isTrait), s"found non-interface among: ${superInterfaces.mkString(", ")}") - - erasure.minimizeInterfaces(superInterfaces.map(_.info)).map(_.typeSymbol) - } - private def buildNestedInfo(innerClassSym: Symbol): Option[NestedInfo] = { assert(innerClassSym.isClass, s"Cannot build NestedInfo for non-class symbol $innerClassSym") diff --git a/src/compiler/scala/tools/nsc/backend/jvm/GenASM.scala b/src/compiler/scala/tools/nsc/backend/jvm/GenASM.scala index e56a20c2e7..7626df312e 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/GenASM.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/GenASM.scala @@ -1206,22 +1206,6 @@ abstract class GenASM extends SubComponent with BytecodeWriters { self => def serialVUID: Option[Long] = genBCode.serialVUID(clasz.symbol) - private def getSuperInterfaces(c: IClass): Array[String] = { - - // Additional interface parents based on annotations and other cues - def newParentForAttr(ann: AnnotationInfo): Symbol = ann.symbol match { - case RemoteAttr => RemoteInterfaceClass - case _ => NoSymbol - } - - val ps = c.symbol.info.parents - val superInterfaces0: List[Symbol] = if(ps.isEmpty) Nil else c.symbol.mixinClasses - val superInterfaces = existingSymbols(superInterfaces0 ++ c.symbol.annotations.map(newParentForAttr)).distinct - - if(superInterfaces.isEmpty) EMPTY_STRING_ARRAY - else mkArray(erasure.minimizeInterfaces(superInterfaces.map(_.info)).map(t => javaName(t.typeSymbol))) - } - var clasz: IClass = _ // this var must be assigned only by genClass() var jclass: asm.ClassWriter = _ // the classfile being emitted var thisName: String = _ // the internal name of jclass @@ -1242,7 +1226,7 @@ abstract class GenASM extends SubComponent with BytecodeWriters { self => val ps = c.symbol.info.parents val superClass: String = if(ps.isEmpty) JAVA_LANG_OBJECT.getInternalName else javaName(ps.head.typeSymbol) - val ifaces = getSuperInterfaces(c) + val ifaces: Array[String] = implementedInterfaces(c.symbol).map(javaName)(collection.breakOut) val thisSignature = getGenericSignature(c.symbol, c.symbol.owner) val flags = mkFlags( diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala index 273112b93c..08f15438fe 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala @@ -55,7 +55,7 @@ class LocalOpt(settings: ScalaSettings) { * @return `true` if unreachable code was elminated in some method, `false` otherwise. */ def methodOptimizations(clazz: ClassNode): Boolean = { - settings.Yopt.value.nonEmpty && clazz.methods.asScala.foldLeft(false) { + !settings.YoptNone && clazz.methods.asScala.foldLeft(false) { case (changed, method) => methodOptimizations(method, clazz.name) || changed } } diff --git a/src/compiler/scala/tools/nsc/backend/opt/Inliners.scala b/src/compiler/scala/tools/nsc/backend/opt/Inliners.scala index 351eb23c4c..aa18b26d93 100644 --- a/src/compiler/scala/tools/nsc/backend/opt/Inliners.scala +++ b/src/compiler/scala/tools/nsc/backend/opt/Inliners.scala @@ -26,7 +26,7 @@ import scala.reflect.internal.util.NoSourceFile * where `p` is defined in a library L, and is accessed from a library C (for Client), * where C was compiled against L', an optimized version of L where the inliner made `p` public at the bytecode level. * The only such members are fields, either synthetic or isParamAccessor, and thus having a dollar sign in their name - * (the accesibility of methods and constructors isn't touched by the inliner). + * (the accessibility of methods and constructors isn't touched by the inliner). * * Thus we add one more goal to our list: * (c) Compile C (either optimized or not) against any of L or L', diff --git a/src/compiler/scala/tools/nsc/classpath/AggregateFlatClassPath.scala b/src/compiler/scala/tools/nsc/classpath/AggregateFlatClassPath.scala new file mode 100644 index 0000000000..3f06264e3c --- /dev/null +++ b/src/compiler/scala/tools/nsc/classpath/AggregateFlatClassPath.scala @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2014 Contributor. All rights reserved. + */ +package scala.tools.nsc.classpath + +import java.net.URL +import scala.annotation.tailrec +import scala.collection.mutable.ArrayBuffer +import scala.reflect.io.AbstractFile +import scala.tools.nsc.util.ClassPath +import scala.tools.nsc.util.ClassRepresentation + +/** + * A classpath unifying multiple class- and sourcepath entries. + * Flat classpath can obtain entries for classes and sources independently + * so it tries to do operations quite optimally - iterating only these collections + * which are needed in the given moment and only as far as it's necessary. + * @param aggregates classpath instances containing entries which this class processes + */ +case class AggregateFlatClassPath(aggregates: Seq[FlatClassPath]) extends FlatClassPath { + + override def findClassFile(className: String): Option[AbstractFile] = { + @tailrec + def find(aggregates: Seq[FlatClassPath]): Option[AbstractFile] = + if (aggregates.nonEmpty) { + val classFile = aggregates.head.findClassFile(className) + if (classFile.isDefined) classFile + else find(aggregates.tail) + } else None + + find(aggregates) + } + + override def findClass(className: String): Option[ClassRepresentation[AbstractFile]] = { + val (pkg, simpleClassName) = PackageNameUtils.separatePkgAndClassNames(className) + + @tailrec + def findEntry[T <: ClassRepClassPathEntry](aggregates: Seq[FlatClassPath], getEntries: FlatClassPath => Seq[T]): Option[T] = + if (aggregates.nonEmpty) { + val entry = getEntries(aggregates.head) + .find(_.name == simpleClassName) + if (entry.isDefined) entry + else findEntry(aggregates.tail, getEntries) + } else None + + val classEntry = findEntry(aggregates, classesGetter(pkg)) + val sourceEntry = findEntry(aggregates, sourcesGetter(pkg)) + + mergeClassesAndSources(classEntry.toList, sourceEntry.toList).headOption + } + + override def asURLs: Seq[URL] = aggregates.flatMap(_.asURLs) + + override def asClassPathStrings: Seq[String] = aggregates.map(_.asClassPathString).distinct + + override def asSourcePathString: String = ClassPath.join(aggregates map (_.asSourcePathString): _*) + + override private[nsc] def packages(inPackage: String): Seq[PackageEntry] = { + val aggregatedPackages = aggregates.flatMap(_.packages(inPackage)).distinct + aggregatedPackages + } + + override private[nsc] def classes(inPackage: String): Seq[ClassFileEntry] = + getDistinctEntries(classesGetter(inPackage)) + + override private[nsc] def sources(inPackage: String): Seq[SourceFileEntry] = + getDistinctEntries(sourcesGetter(inPackage)) + + override private[nsc] def list(inPackage: String): FlatClassPathEntries = { + val (packages, classesAndSources) = aggregates.map(_.list(inPackage)).unzip + val distinctPackages = packages.flatten.distinct + val distinctClassesAndSources = mergeClassesAndSources(classesAndSources: _*) + FlatClassPathEntries(distinctPackages, distinctClassesAndSources) + } + + /** + * Returns only one entry for each name. If there's both a source and a class entry, it + * creates an entry containing both of them. If there would be more than one class or source + * entries for the same class it always would use the first entry of each type found on a classpath. + */ + private def mergeClassesAndSources(entries: Seq[ClassRepClassPathEntry]*): Seq[ClassRepClassPathEntry] = { + // based on the implementation from MergedClassPath + var count = 0 + val indices = collection.mutable.HashMap[String, Int]() + val mergedEntries = new ArrayBuffer[ClassRepClassPathEntry](1024) + + for { + partOfEntries <- entries + entry <- partOfEntries + } { + val name = entry.name + if (indices contains name) { + val index = indices(name) + val existing = mergedEntries(index) + + if (existing.binary.isEmpty && entry.binary.isDefined) + mergedEntries(index) = ClassAndSourceFilesEntry(entry.binary.get, existing.source.get) + if (existing.source.isEmpty && entry.source.isDefined) + mergedEntries(index) = ClassAndSourceFilesEntry(existing.binary.get, entry.source.get) + } + else { + indices(name) = count + mergedEntries += entry + count += 1 + } + } + mergedEntries.toIndexedSeq + } + + private def getDistinctEntries[EntryType <: ClassRepClassPathEntry](getEntries: FlatClassPath => Seq[EntryType]): Seq[EntryType] = { + val seenNames = collection.mutable.HashSet[String]() + val entriesBuffer = new ArrayBuffer[EntryType](1024) + for { + cp <- aggregates + entry <- getEntries(cp) if !seenNames.contains(entry.name) + } { + entriesBuffer += entry + seenNames += entry.name + } + entriesBuffer.toIndexedSeq + } + + private def classesGetter(pkg: String) = (cp: FlatClassPath) => cp.classes(pkg) + private def sourcesGetter(pkg: String) = (cp: FlatClassPath) => cp.sources(pkg) +} diff --git a/src/compiler/scala/tools/nsc/classpath/ClassPathFactory.scala b/src/compiler/scala/tools/nsc/classpath/ClassPathFactory.scala new file mode 100644 index 0000000000..9bf4e3f779 --- /dev/null +++ b/src/compiler/scala/tools/nsc/classpath/ClassPathFactory.scala @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2014 Contributor. All rights reserved. + */ +package scala.tools.nsc.classpath + +import scala.reflect.io.AbstractFile +import scala.tools.nsc.util.ClassPath + +/** + * A trait that contains factory methods for classpath elements of type T. + * + * The logic has been abstracted from ClassPath#ClassPathContext so it's possible + * to have common trait that supports both recursive and flat classpath representations. + * + * Therefore, we expect that T will be either ClassPath[U] or FlatClassPath. + */ +trait ClassPathFactory[T] { + + /** + * Create a new classpath based on the abstract file. + */ + def newClassPath(file: AbstractFile): T + + /** + * Creators for sub classpaths which preserve this context. + */ + def sourcesInPath(path: String): List[T] + + def expandPath(path: String, expandStar: Boolean = true): List[String] = ClassPath.expandPath(path, expandStar) + + def expandDir(extdir: String): List[String] = ClassPath.expandDir(extdir) + + def contentsOfDirsInPath(path: String): List[T] = + for { + dir <- expandPath(path, expandStar = false) + name <- expandDir(dir) + entry <- Option(AbstractFile.getDirectory(name)) + } yield newClassPath(entry) + + def classesInExpandedPath(path: String): IndexedSeq[T] = + classesInPathImpl(path, expand = true).toIndexedSeq + + def classesInPath(path: String) = classesInPathImpl(path, expand = false) + + def classesInManifest(useManifestClassPath: Boolean) = + if (useManifestClassPath) ClassPath.manifests.map(url => newClassPath(AbstractFile getResources url)) + else Nil + + // Internal + protected def classesInPathImpl(path: String, expand: Boolean) = + for { + file <- expandPath(path, expand) + dir <- Option(AbstractFile.getDirectory(file)) + } yield newClassPath(dir) +} diff --git a/src/compiler/scala/tools/nsc/classpath/DirectoryFlatClassPath.scala b/src/compiler/scala/tools/nsc/classpath/DirectoryFlatClassPath.scala new file mode 100644 index 0000000000..81d2f7320f --- /dev/null +++ b/src/compiler/scala/tools/nsc/classpath/DirectoryFlatClassPath.scala @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2014 Contributor. All rights reserved. + */ +package scala.tools.nsc.classpath + +import java.io.File +import java.io.FileFilter +import java.net.URL +import scala.reflect.io.AbstractFile +import scala.reflect.io.PlainFile +import scala.tools.nsc.util.ClassRepresentation +import FileUtils._ + +/** + * A trait allowing to look for classpath entries of given type in directories. + * It provides common logic for classes handling class and source files. + * It makes use of the fact that in the case of nested directories it's easy to find a file + * when we have a name of a package. + */ +trait DirectoryFileLookup[FileEntryType <: ClassRepClassPathEntry] extends FlatClassPath { + val dir: File + assert(dir != null, "Directory file in DirectoryFileLookup cannot be null") + + override def asURLs: Seq[URL] = Seq(dir.toURI.toURL) + override def asClassPathStrings: Seq[String] = Seq(dir.getPath) + + import FlatClassPath.RootPackage + private def getDirectory(forPackage: String): Option[File] = { + if (forPackage == RootPackage) { + Some(dir) + } else { + val packageDirName = FileUtils.dirPath(forPackage) + val packageDir = new File(dir, packageDirName) + if (packageDir.exists && packageDir.isDirectory) { + Some(packageDir) + } else None + } + } + + override private[nsc] def packages(inPackage: String): Seq[PackageEntry] = { + val dirForPackage = getDirectory(inPackage) + val nestedDirs: Array[File] = dirForPackage match { + case None => Array.empty + case Some(directory) => directory.listFiles(DirectoryFileLookup.packageDirectoryFileFilter) + } + val prefix = PackageNameUtils.packagePrefix(inPackage) + val entries = nestedDirs map { file => + PackageEntryImpl(prefix + file.getName) + } + entries + } + + protected def files(inPackage: String): Seq[FileEntryType] = { + val dirForPackage = getDirectory(inPackage) + val files: Array[File] = dirForPackage match { + case None => Array.empty + case Some(directory) => directory.listFiles(fileFilter) + } + val entries = files map { file => + val wrappedFile = new scala.reflect.io.File(file) + createFileEntry(new PlainFile(wrappedFile)) + } + entries + } + + override private[nsc] def list(inPackage: String): FlatClassPathEntries = { + val dirForPackage = getDirectory(inPackage) + val files: Array[File] = dirForPackage match { + case None => Array.empty + case Some(directory) => directory.listFiles() + } + val packagePrefix = PackageNameUtils.packagePrefix(inPackage) + val packageBuf = collection.mutable.ArrayBuffer.empty[PackageEntry] + val fileBuf = collection.mutable.ArrayBuffer.empty[FileEntryType] + for (file <- files) { + if (file.isPackage) { + val pkgEntry = PackageEntryImpl(packagePrefix + file.getName) + packageBuf += pkgEntry + } else if (fileFilter.accept(file)) { + val wrappedFile = new scala.reflect.io.File(file) + val abstractFile = new PlainFile(wrappedFile) + fileBuf += createFileEntry(abstractFile) + } + } + FlatClassPathEntries(packageBuf, fileBuf) + } + + protected def createFileEntry(file: AbstractFile): FileEntryType + protected def fileFilter: FileFilter +} + +object DirectoryFileLookup { + + private[classpath] object packageDirectoryFileFilter extends FileFilter { + override def accept(pathname: File): Boolean = pathname.isPackage + } +} + +case class DirectoryFlatClassPath(dir: File) + extends DirectoryFileLookup[ClassFileEntryImpl] + with NoSourcePaths { + + override def findClass(className: String): Option[ClassRepresentation[AbstractFile]] = findClassFile(className) map ClassFileEntryImpl + + override def findClassFile(className: String): Option[AbstractFile] = { + val relativePath = FileUtils.dirPath(className) + val classFile = new File(s"$dir/$relativePath.class") + if (classFile.exists) { + val wrappedClassFile = new scala.reflect.io.File(classFile) + val abstractClassFile = new PlainFile(wrappedClassFile) + Some(abstractClassFile) + } else None + } + + override protected def createFileEntry(file: AbstractFile): ClassFileEntryImpl = ClassFileEntryImpl(file) + override protected def fileFilter: FileFilter = DirectoryFlatClassPath.classFileFilter + + override private[nsc] def classes(inPackage: String): Seq[ClassFileEntry] = files(inPackage) +} + +object DirectoryFlatClassPath { + + private val classFileFilter = new FileFilter { + override def accept(pathname: File): Boolean = pathname.isClass + } +} + +case class DirectoryFlatSourcePath(dir: File) + extends DirectoryFileLookup[SourceFileEntryImpl] + with NoClassPaths { + + override def asSourcePathString: String = asClassPathString + + override protected def createFileEntry(file: AbstractFile): SourceFileEntryImpl = SourceFileEntryImpl(file) + override protected def fileFilter: FileFilter = DirectoryFlatSourcePath.sourceFileFilter + + override def findClass(className: String): Option[ClassRepresentation[AbstractFile]] = { + findSourceFile(className) map SourceFileEntryImpl + } + + private def findSourceFile(className: String): Option[AbstractFile] = { + val relativePath = FileUtils.dirPath(className) + val sourceFile = Stream("scala", "java") + .map(ext => new File(s"$dir/$relativePath.$ext")) + .collectFirst { case file if file.exists() => file } + + sourceFile.map { file => + val wrappedSourceFile = new scala.reflect.io.File(file) + val abstractSourceFile = new PlainFile(wrappedSourceFile) + abstractSourceFile + } + } + + override private[nsc] def sources(inPackage: String): Seq[SourceFileEntry] = files(inPackage) +} + +object DirectoryFlatSourcePath { + + private val sourceFileFilter = new FileFilter { + override def accept(pathname: File): Boolean = endsScalaOrJava(pathname.getName) + } +} diff --git a/src/compiler/scala/tools/nsc/classpath/FileUtils.scala b/src/compiler/scala/tools/nsc/classpath/FileUtils.scala new file mode 100644 index 0000000000..ee2528e15c --- /dev/null +++ b/src/compiler/scala/tools/nsc/classpath/FileUtils.scala @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2014 Contributor. All rights reserved. + */ +package scala.tools.nsc.classpath + +import java.io.{ File => JFile } +import java.net.URL +import scala.reflect.internal.FatalError +import scala.reflect.io.AbstractFile + +/** + * Common methods related to Java files and abstract files used in the context of classpath + */ +object FileUtils { + implicit class AbstractFileOps(val file: AbstractFile) extends AnyVal { + def isPackage: Boolean = file.isDirectory && mayBeValidPackage(file.name) + + def isClass: Boolean = !file.isDirectory && file.hasExtension("class") + + def isScalaOrJavaSource: Boolean = !file.isDirectory && (file.hasExtension("scala") || file.hasExtension("java")) + + // TODO do we need to check also other files using ZipMagicNumber like in scala.tools.nsc.io.Jar.isJarOrZip? + def isJarOrZip: Boolean = file.hasExtension("jar") || file.hasExtension("zip") + + /** + * Safe method returning a sequence containing one URL representing this file, when underlying file exists, + * and returning given default value in other case + */ + def toURLs(default: => Seq[URL] = Seq.empty): Seq[URL] = if (file.file == null) default else Seq(file.toURL) + } + + implicit class FileOps(val file: JFile) extends AnyVal { + def isPackage: Boolean = file.isDirectory && mayBeValidPackage(file.getName) + + def isClass: Boolean = file.isFile && file.getName.endsWith(".class") + } + + def stripSourceExtension(fileName: String): String = { + if (endsScala(fileName)) stripClassExtension(fileName) + else if (endsJava(fileName)) stripJavaExtension(fileName) + else throw new FatalError("Unexpected source file ending: " + fileName) + } + + def dirPath(forPackage: String) = forPackage.replace('.', '/') + + def endsClass(fileName: String): Boolean = + fileName.length > 6 && fileName.substring(fileName.length - 6) == ".class" + + def endsScalaOrJava(fileName: String): Boolean = + endsScala(fileName) || endsJava(fileName) + + def endsJava(fileName: String): Boolean = + fileName.length > 5 && fileName.substring(fileName.length - 5) == ".java" + + def endsScala(fileName: String): Boolean = + fileName.length > 6 && fileName.substring(fileName.length - 6) == ".scala" + + def stripClassExtension(fileName: String): String = + fileName.substring(0, fileName.length - 6) // equivalent of fileName.length - ".class".length + + def stripJavaExtension(fileName: String): String = + fileName.substring(0, fileName.length - 5) + + // probably it should match a pattern like [a-z_]{1}[a-z0-9_]* but it cannot be changed + // because then some tests in partest don't pass + private def mayBeValidPackage(dirName: String): Boolean = + (dirName != "META-INF") && (dirName != "") && (dirName.charAt(0) != '.') +} diff --git a/src/compiler/scala/tools/nsc/classpath/FlatClassPath.scala b/src/compiler/scala/tools/nsc/classpath/FlatClassPath.scala new file mode 100644 index 0000000000..26b5429e23 --- /dev/null +++ b/src/compiler/scala/tools/nsc/classpath/FlatClassPath.scala @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2014 Contributor. All rights reserved. + */ +package scala.tools.nsc.classpath + +import scala.reflect.io.AbstractFile +import scala.tools.nsc.util.{ ClassFileLookup, ClassPath, ClassRepresentation } + +/** + * A base trait for the particular flat classpath representation implementations. + * + * We call this variant of a classpath representation flat because it's possible to + * query the whole classpath using just single instance extending this trait. + * + * This is an alternative design compared to scala.tools.nsc.util.ClassPath + */ +trait FlatClassPath extends ClassFileLookup[AbstractFile] { + /** Empty string represents root package */ + private[nsc] def packages(inPackage: String): Seq[PackageEntry] + private[nsc] def classes(inPackage: String): Seq[ClassFileEntry] + private[nsc] def sources(inPackage: String): Seq[SourceFileEntry] + + /** Allows to get entries for packages and classes merged with sources possibly in one pass. */ + private[nsc] def list(inPackage: String): FlatClassPathEntries + + // A default implementation which should be overriden, if we can create more efficient + // solution for given type of FlatClassPath + override def findClass(className: String): Option[ClassRepresentation[AbstractFile]] = { + val (pkg, simpleClassName) = PackageNameUtils.separatePkgAndClassNames(className) + + val foundClassFromClassFiles = classes(pkg) + .find(_.name == simpleClassName) + + def findClassInSources = sources(pkg) + .find(_.name == simpleClassName) + + foundClassFromClassFiles orElse findClassInSources + } + + override def asClassPathString: String = ClassPath.join(asClassPathStrings: _*) + def asClassPathStrings: Seq[String] +} + +object FlatClassPath { + val RootPackage = "" +} + +case class FlatClassPathEntries(packages: Seq[PackageEntry], classesAndSources: Seq[ClassRepClassPathEntry]) + +object FlatClassPathEntries { + import scala.language.implicitConversions + // to have working unzip method + implicit def entry2Tuple(entry: FlatClassPathEntries) = (entry.packages, entry.classesAndSources) +} + +sealed trait ClassRepClassPathEntry extends ClassRepresentation[AbstractFile] + +trait ClassFileEntry extends ClassRepClassPathEntry { + def file: AbstractFile +} + +trait SourceFileEntry extends ClassRepClassPathEntry { + def file: AbstractFile +} + +trait PackageEntry { + def name: String +} + +private[nsc] case class ClassFileEntryImpl(file: AbstractFile) extends ClassFileEntry { + override def name = FileUtils.stripClassExtension(file.name) // class name + + override def binary: Option[AbstractFile] = Some(file) + override def source: Option[AbstractFile] = None +} + +private[nsc] case class SourceFileEntryImpl(file: AbstractFile) extends SourceFileEntry { + override def name = FileUtils.stripSourceExtension(file.name) + + override def binary: Option[AbstractFile] = None + override def source: Option[AbstractFile] = Some(file) +} + +private[nsc] case class ClassAndSourceFilesEntry(classFile: AbstractFile, srcFile: AbstractFile) extends ClassRepClassPathEntry { + override def name = FileUtils.stripClassExtension(classFile.name) + + override def binary: Option[AbstractFile] = Some(classFile) + override def source: Option[AbstractFile] = Some(srcFile) +} + +private[nsc] case class PackageEntryImpl(name: String) extends PackageEntry + +private[nsc] trait NoSourcePaths { + def asSourcePathString: String = "" + private[nsc] def sources(inPackage: String): Seq[SourceFileEntry] = Seq.empty +} + +private[nsc] trait NoClassPaths { + def findClassFile(className: String): Option[AbstractFile] = None + private[nsc] def classes(inPackage: String): Seq[ClassFileEntry] = Seq.empty +} diff --git a/src/compiler/scala/tools/nsc/classpath/FlatClassPathFactory.scala b/src/compiler/scala/tools/nsc/classpath/FlatClassPathFactory.scala new file mode 100644 index 0000000000..7f67381d4d --- /dev/null +++ b/src/compiler/scala/tools/nsc/classpath/FlatClassPathFactory.scala @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2014 Contributor. All rights reserved. + */ +package scala.tools.nsc.classpath + +import scala.tools.nsc.Settings +import scala.tools.nsc.io.AbstractFile +import scala.tools.nsc.util.ClassPath +import FileUtils.AbstractFileOps + +/** + * Provides factory methods for flat classpath. When creating classpath instances for a given path, + * it uses proper type of classpath depending on a types of particular files containing sources or classes. + */ +class FlatClassPathFactory(settings: Settings) extends ClassPathFactory[FlatClassPath] { + + override def newClassPath(file: AbstractFile): FlatClassPath = + if (file.isJarOrZip) + ZipAndJarFlatClassPathFactory.create(file, settings) + else if (file.isDirectory) + new DirectoryFlatClassPath(file.file) + else + sys.error(s"Unsupported classpath element: $file") + + override def sourcesInPath(path: String): List[FlatClassPath] = + for { + file <- expandPath(path, expandStar = false) + dir <- Option(AbstractFile getDirectory file) + } yield createSourcePath(dir) + + private def createSourcePath(file: AbstractFile): FlatClassPath = + if (file.isJarOrZip) + ZipAndJarFlatSourcePathFactory.create(file, settings) + else if (file.isDirectory) + new DirectoryFlatSourcePath(file.file) + else + sys.error(s"Unsupported sourcepath element: $file") +} diff --git a/src/compiler/scala/tools/nsc/classpath/PackageNameUtils.scala b/src/compiler/scala/tools/nsc/classpath/PackageNameUtils.scala new file mode 100644 index 0000000000..c907d565d2 --- /dev/null +++ b/src/compiler/scala/tools/nsc/classpath/PackageNameUtils.scala @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2014 Contributor. All rights reserved. + */ +package scala.tools.nsc.classpath + +import scala.tools.nsc.classpath.FlatClassPath.RootPackage + +/** + * Common methods related to package names represented as String + */ +object PackageNameUtils { + + /** + * @param fullClassName full class name with package + * @return (package, simple class name) + */ + def separatePkgAndClassNames(fullClassName: String): (String, String) = { + val lastDotIndex = fullClassName.lastIndexOf('.') + if (lastDotIndex == -1) + (RootPackage, fullClassName) + else + (fullClassName.substring(0, lastDotIndex), fullClassName.substring(lastDotIndex + 1)) + } + + def packagePrefix(inPackage: String): String = if (inPackage == RootPackage) "" else inPackage + "." +} diff --git a/src/compiler/scala/tools/nsc/classpath/ZipAndJarFileLookupFactory.scala b/src/compiler/scala/tools/nsc/classpath/ZipAndJarFileLookupFactory.scala new file mode 100644 index 0000000000..84e21a3ccd --- /dev/null +++ b/src/compiler/scala/tools/nsc/classpath/ZipAndJarFileLookupFactory.scala @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2014 Contributor. All rights reserved. + */ +package scala.tools.nsc.classpath + +import java.io.File +import java.net.URL +import scala.annotation.tailrec +import scala.reflect.io.{ AbstractFile, FileZipArchive, ManifestResources } +import scala.tools.nsc.Settings +import FileUtils._ + +/** + * A trait providing an optional cache for classpath entries obtained from zip and jar files. + * It's possible to create such a cache assuming that entries in such files won't change (at + * least will be the same each time we'll load classpath during the lifetime of JVM process) + * - unlike class and source files in directories, which can be modified and recompiled. + * It allows us to e.g. reduce significantly memory used by PresentationCompilers in Scala IDE + * when there are a lot of projects having a lot of common dependencies. + */ +sealed trait ZipAndJarFileLookupFactory { + + private val cache = collection.mutable.Map.empty[AbstractFile, FlatClassPath] + + def create(zipFile: AbstractFile, settings: Settings): FlatClassPath = { + if (settings.YdisableFlatCpCaching) createForZipFile(zipFile) + else createUsingCache(zipFile, settings) + } + + protected def createForZipFile(zipFile: AbstractFile): FlatClassPath + + private def createUsingCache(zipFile: AbstractFile, settings: Settings): FlatClassPath = cache.synchronized { + def newClassPathInstance = { + if (settings.verbose || settings.Ylogcp) + println(s"$zipFile is not yet in the classpath cache") + createForZipFile(zipFile) + } + cache.getOrElseUpdate(zipFile, newClassPathInstance) + } +} + +/** + * Manages creation of flat classpath for class files placed in zip and jar files. + * It should be the only way of creating them as it provides caching. + */ +object ZipAndJarFlatClassPathFactory extends ZipAndJarFileLookupFactory { + + private case class ZipArchiveFlatClassPath(zipFile: File) + extends ZipArchiveFileLookup[ClassFileEntryImpl] + with NoSourcePaths { + + override def findClassFile(className: String): Option[AbstractFile] = { + val (pkg, simpleClassName) = PackageNameUtils.separatePkgAndClassNames(className) + classes(pkg).find(_.name == simpleClassName).map(_.file) + } + + override private[nsc] def classes(inPackage: String): Seq[ClassFileEntry] = files(inPackage) + + override protected def createFileEntry(file: FileZipArchive#Entry): ClassFileEntryImpl = ClassFileEntryImpl(file) + override protected def isRequiredFileType(file: AbstractFile): Boolean = file.isClass + } + + /** + * This type of classpath is closly related to the support for JSR-223. + * Its usage can be observed e.g. when running: + * jrunscript -classpath scala-compiler.jar;scala-reflect.jar;scala-library.jar -l scala + * with a particularly prepared scala-library.jar. It should have all classes listed in the manifest like e.g. this entry: + * Name: scala/Function2$mcFJD$sp.class + */ + private case class ManifestResourcesFlatClassPath(file: ManifestResources) + extends FlatClassPath + with NoSourcePaths { + + override def findClassFile(className: String): Option[AbstractFile] = { + val (pkg, simpleClassName) = PackageNameUtils.separatePkgAndClassNames(className) + classes(pkg).find(_.name == simpleClassName).map(_.file) + } + + override def asClassPathStrings: Seq[String] = Seq(file.path) + + override def asURLs: Seq[URL] = file.toURLs() + + import ManifestResourcesFlatClassPath.PackageFileInfo + import ManifestResourcesFlatClassPath.PackageInfo + + /** + * A cache mapping package name to abstract file for package directory and subpackages of given package. + * + * ManifestResources can iterate through the collections of entries from e.g. remote jar file. + * We can't just specify the path to the concrete directory etc. so we can't just 'jump' into + * given package, when it's needed. On the other hand we can iterate over entries to get + * AbstractFiles, iterate over entries of these files etc. + * + * Instead of traversing a tree of AbstractFiles once and caching all entries or traversing each time, + * when we need subpackages of a given package or its classes, we traverse once and cache only packages. + * Classes for given package can be then easily loaded when they are needed. + */ + private lazy val cachedPackages: collection.mutable.HashMap[String, PackageFileInfo] = { + val packages = collection.mutable.HashMap[String, PackageFileInfo]() + + def getSubpackages(dir: AbstractFile): List[AbstractFile] = + (for (file <- dir if file.isPackage) yield file)(collection.breakOut) + + @tailrec + def traverse(packagePrefix: String, + filesForPrefix: List[AbstractFile], + subpackagesQueue: collection.mutable.Queue[PackageInfo]): Unit = filesForPrefix match { + case pkgFile :: remainingFiles => + val subpackages = getSubpackages(pkgFile) + val fullPkgName = packagePrefix + pkgFile.name + packages.put(fullPkgName, PackageFileInfo(pkgFile, subpackages)) + val newPackagePrefix = fullPkgName + "." + subpackagesQueue.enqueue(PackageInfo(newPackagePrefix, subpackages)) + traverse(packagePrefix, remainingFiles, subpackagesQueue) + case Nil if subpackagesQueue.nonEmpty => + val PackageInfo(packagePrefix, filesForPrefix) = subpackagesQueue.dequeue() + traverse(packagePrefix, filesForPrefix, subpackagesQueue) + case _ => + } + + val subpackages = getSubpackages(file) + packages.put(FlatClassPath.RootPackage, PackageFileInfo(file, subpackages)) + traverse(FlatClassPath.RootPackage, subpackages, collection.mutable.Queue()) + packages + } + + override private[nsc] def packages(inPackage: String): Seq[PackageEntry] = cachedPackages.get(inPackage) match { + case None => Seq.empty + case Some(PackageFileInfo(_, subpackages)) => + val prefix = PackageNameUtils.packagePrefix(inPackage) + subpackages.map(packageFile => PackageEntryImpl(prefix + packageFile.name)) + } + + override private[nsc] def classes(inPackage: String): Seq[ClassFileEntry] = cachedPackages.get(inPackage) match { + case None => Seq.empty + case Some(PackageFileInfo(pkg, _)) => + (for (file <- pkg if file.isClass) yield ClassFileEntryImpl(file))(collection.breakOut) + } + + override private[nsc] def list(inPackage: String): FlatClassPathEntries = FlatClassPathEntries(packages(inPackage), classes(inPackage)) + } + + private object ManifestResourcesFlatClassPath { + case class PackageFileInfo(packageFile: AbstractFile, subpackages: Seq[AbstractFile]) + case class PackageInfo(packageName: String, subpackages: List[AbstractFile]) + } + + override protected def createForZipFile(zipFile: AbstractFile): FlatClassPath = + if (zipFile.file == null) createWithoutUnderlyingFile(zipFile) + else ZipArchiveFlatClassPath(zipFile.file) + + private def createWithoutUnderlyingFile(zipFile: AbstractFile) = zipFile match { + case manifestRes: ManifestResources => + ManifestResourcesFlatClassPath(manifestRes) + case _ => + val errorMsg = s"Abstract files which don't have an underlying file and are not ManifestResources are not supported. There was $zipFile" + throw new IllegalArgumentException(errorMsg) + } +} + +/** + * Manages creation of flat classpath for source files placed in zip and jar files. + * It should be the only way of creating them as it provides caching. + */ +object ZipAndJarFlatSourcePathFactory extends ZipAndJarFileLookupFactory { + + private case class ZipArchiveFlatSourcePath(zipFile: File) + extends ZipArchiveFileLookup[SourceFileEntryImpl] + with NoClassPaths { + + override def asSourcePathString: String = asClassPathString + + override private[nsc] def sources(inPackage: String): Seq[SourceFileEntry] = files(inPackage) + + override protected def createFileEntry(file: FileZipArchive#Entry): SourceFileEntryImpl = SourceFileEntryImpl(file) + override protected def isRequiredFileType(file: AbstractFile): Boolean = file.isScalaOrJavaSource + } + + override protected def createForZipFile(zipFile: AbstractFile): FlatClassPath = ZipArchiveFlatSourcePath(zipFile.file) +} diff --git a/src/compiler/scala/tools/nsc/classpath/ZipArchiveFileLookup.scala b/src/compiler/scala/tools/nsc/classpath/ZipArchiveFileLookup.scala new file mode 100644 index 0000000000..1d0de57779 --- /dev/null +++ b/src/compiler/scala/tools/nsc/classpath/ZipArchiveFileLookup.scala @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2014 Contributor. All rights reserved. + */ +package scala.tools.nsc.classpath + +import java.io.File +import java.net.URL +import scala.collection.Seq +import scala.reflect.io.AbstractFile +import scala.reflect.io.FileZipArchive +import FileUtils.AbstractFileOps + +/** + * A trait allowing to look for classpath entries of given type in zip and jar files. + * It provides common logic for classes handling class and source files. + * It's aware of things like e.g. META-INF directory which is correctly skipped. + */ +trait ZipArchiveFileLookup[FileEntryType <: ClassRepClassPathEntry] extends FlatClassPath { + val zipFile: File + + assert(zipFile != null, "Zip file in ZipArchiveFileLookup cannot be null") + + override def asURLs: Seq[URL] = Seq(zipFile.toURI.toURL) + override def asClassPathStrings: Seq[String] = Seq(zipFile.getPath) + + private val archive = new FileZipArchive(zipFile) + + override private[nsc] def packages(inPackage: String): Seq[PackageEntry] = { + val prefix = PackageNameUtils.packagePrefix(inPackage) + for { + dirEntry <- findDirEntry(inPackage).toSeq + entry <- dirEntry.iterator if entry.isPackage + } yield PackageEntryImpl(prefix + entry.name) + } + + protected def files(inPackage: String): Seq[FileEntryType] = + for { + dirEntry <- findDirEntry(inPackage).toSeq + entry <- dirEntry.iterator if isRequiredFileType(entry) + } yield createFileEntry(entry) + + override private[nsc] def list(inPackage: String): FlatClassPathEntries = { + val foundDirEntry = findDirEntry(inPackage) + + foundDirEntry map { dirEntry => + val pkgBuf = collection.mutable.ArrayBuffer.empty[PackageEntry] + val fileBuf = collection.mutable.ArrayBuffer.empty[FileEntryType] + val prefix = PackageNameUtils.packagePrefix(inPackage) + + for (entry <- dirEntry.iterator) { + if (entry.isPackage) + pkgBuf += PackageEntryImpl(prefix + entry.name) + else if (isRequiredFileType(entry)) + fileBuf += createFileEntry(entry) + } + FlatClassPathEntries(pkgBuf, fileBuf) + } getOrElse FlatClassPathEntries(Seq.empty, Seq.empty) + } + + private def findDirEntry(pkg: String) = { + val dirName = s"${FileUtils.dirPath(pkg)}/" + archive.allDirs.get(dirName) + } + + protected def createFileEntry(file: FileZipArchive#Entry): FileEntryType + protected def isRequiredFileType(file: AbstractFile): Boolean +} diff --git a/src/compiler/scala/tools/nsc/plugins/Plugins.scala b/src/compiler/scala/tools/nsc/plugins/Plugins.scala index 6e3d013e52..4b1805479d 100644 --- a/src/compiler/scala/tools/nsc/plugins/Plugins.scala +++ b/src/compiler/scala/tools/nsc/plugins/Plugins.scala @@ -7,7 +7,7 @@ package scala.tools.nsc package plugins -import scala.reflect.io.{ File, Path } +import scala.reflect.io.Path import scala.tools.nsc.util.ClassPath import scala.tools.util.PathResolver.Defaults diff --git a/src/compiler/scala/tools/nsc/settings/FscSettings.scala b/src/compiler/scala/tools/nsc/settings/FscSettings.scala index 8c2b510bfd..fffbb4333f 100644 --- a/src/compiler/scala/tools/nsc/settings/FscSettings.scala +++ b/src/compiler/scala/tools/nsc/settings/FscSettings.scala @@ -22,13 +22,15 @@ class FscSettings(error: String => Unit) extends Settings(error) { val reset = BooleanSetting("-reset", "Reset compile server caches") val shutdown = BooleanSetting("-shutdown", "Shutdown compile server") val server = StringSetting ("-server", "hostname:portnumber", "Specify compile server socket", "") + val port = IntSetting ("-port", "Search and start compile server in given port only", + 0, Some((0, Int.MaxValue)), (_: String) => None) val preferIPv4 = BooleanSetting("-ipv4", "Use IPv4 rather than IPv6 for the server socket") val idleMins = IntSetting ("-max-idle", "Set idle timeout in minutes for fsc (use 0 for no timeout)", 30, Some((0, Int.MaxValue)), (_: String) => None) // For improved help output, separating fsc options from the others. def fscSpecific = Set[Settings#Setting]( - currentDir, reset, shutdown, server, preferIPv4, idleMins + currentDir, reset, shutdown, server, port, preferIPv4, idleMins ) val isFscSpecific: String => Boolean = fscSpecific map (_.name) diff --git a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala index d6650595eb..fc02f6ff56 100644 --- a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala +++ b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala @@ -202,6 +202,9 @@ trait ScalaSettings extends AbsScalaSettings val YmethodInfer = BooleanSetting ("-Yinfer-argument-types", "Infer types for arguments of overriden methods.") val etaExpandKeepsStar = BooleanSetting ("-Yeta-expand-keeps-star", "Eta-expand varargs methods to T* rather than Seq[T]. This is a temporary option to ease transition.").withDeprecationMessage(removalIn212) val inferByName = BooleanSetting ("-Yinfer-by-name", "Allow inference of by-name types. This is a temporary option to ease transition. See SI-7899.").withDeprecationMessage(removalIn212) + val YclasspathImpl = ChoiceSetting ("-YclasspathImpl", "implementation", "Choose classpath scanning method.", List(ClassPathRepresentationType.Recursive, ClassPathRepresentationType.Flat), ClassPathRepresentationType.Recursive) + val YdisableFlatCpCaching = BooleanSetting ("-YdisableFlatCpCaching", "Do not cache flat classpath representation of classpath elements from jars across compiler instances.") + val YvirtClasses = false // too embryonic to even expose as a -Y //BooleanSetting ("-Yvirtual-classes", "Support virtual classes") val YdisableUnreachablePrevention = BooleanSetting("-Ydisable-unreachable-prevention", "Disable the prevention of unreachable blocks in code generation.") val YnoLoadImplClass = BooleanSetting ("-Yno-load-impl-class", "Do not load $class.class files.") @@ -239,6 +242,7 @@ trait ScalaSettings extends AbsScalaSettings descr = "Enable optimizations", domain = YoptChoices) + def YoptNone = Yopt.isSetByUser && Yopt.value.isEmpty def YoptUnreachableCode = !Yopt.isSetByUser || Yopt.contains(YoptChoices.unreachableCode) def YoptSimplifyJumps = Yopt.contains(YoptChoices.simplifyJumps) def YoptRecurseUnreachableJumps = Yopt.contains(YoptChoices.recurseUnreachableJumps) @@ -329,3 +333,8 @@ trait ScalaSettings extends AbsScalaSettings val Discard = "discard" } } + +object ClassPathRepresentationType { + val Flat = "flat" + val Recursive = "recursive" +} diff --git a/src/compiler/scala/tools/nsc/symtab/SymbolLoaders.scala b/src/compiler/scala/tools/nsc/symtab/SymbolLoaders.scala index 82c2a4d6ed..9af3efbece 100644 --- a/src/compiler/scala/tools/nsc/symtab/SymbolLoaders.scala +++ b/src/compiler/scala/tools/nsc/symtab/SymbolLoaders.scala @@ -6,13 +6,15 @@ package scala.tools.nsc package symtab +import classfile.ClassfileParser import java.io.IOException import scala.compat.Platform.currentTime -import scala.tools.nsc.util.{ ClassPath } -import classfile.ClassfileParser import scala.reflect.internal.MissingRequirementError import scala.reflect.internal.util.Statistics import scala.reflect.io.{ AbstractFile, NoAbstractFile } +import scala.tools.nsc.classpath.FlatClassPath +import scala.tools.nsc.settings.ClassPathRepresentationType +import scala.tools.nsc.util.{ ClassPath, ClassRepresentation } /** This class ... * @@ -86,8 +88,7 @@ abstract class SymbolLoaders { // require yjp.jar at runtime. See SI-2089. if (settings.termConflict.isDefault) throw new TypeError( - root+" contains object and package with same name: "+ - name+"\none of them needs to be removed from classpath" + s"$root contains object and package with same name: $name\none of them needs to be removed from classpath" ) else if (settings.termConflict.value == "package") { warning( @@ -154,7 +155,7 @@ abstract class SymbolLoaders { /** Initialize toplevel class and module symbols in `owner` from class path representation `classRep` */ - def initializeFromClassPath(owner: Symbol, classRep: ClassPath[AbstractFile]#ClassRep) { + def initializeFromClassPath(owner: Symbol, classRep: ClassRepresentation[AbstractFile]) { ((classRep.binary, classRep.source) : @unchecked) match { case (Some(bin), Some(src)) if platform.needCompile(bin, src) && !binaryOnly(owner, classRep.name) => @@ -250,7 +251,7 @@ abstract class SymbolLoaders { * Load contents of a package */ class PackageLoader(classpath: ClassPath[AbstractFile]) extends SymbolLoader with FlagAgnosticCompleter { - protected def description = "package loader "+ classpath.name + protected def description = s"package loader ${classpath.name}" protected def doComplete(root: Symbol) { assert(root.isPackageClass, root) @@ -276,6 +277,39 @@ abstract class SymbolLoaders { } } + /** + * Loads contents of a package + */ + class PackageLoaderUsingFlatClassPath(packageName: String, classPath: FlatClassPath) extends SymbolLoader with FlagAgnosticCompleter { + protected def description = { + val shownPackageName = if (packageName == FlatClassPath.RootPackage) "<root package>" else packageName + s"package loader $shownPackageName" + } + + protected def doComplete(root: Symbol) { + assert(root.isPackageClass, root) + root.setInfo(new PackageClassInfoType(newScope, root)) + + val classPathEntries = classPath.list(packageName) + + if (!root.isRoot) + for (entry <- classPathEntries.classesAndSources) initializeFromClassPath(root, entry) + if (!root.isEmptyPackageClass) { + for (pkg <- classPathEntries.packages) { + val fullName = pkg.name + + val name = + if (packageName == FlatClassPath.RootPackage) fullName + else fullName.substring(packageName.length + 1) + val packageLoader = new PackageLoaderUsingFlatClassPath(fullName, classPath) + enterPackage(root, name, packageLoader) + } + + openPackageModule(root) + } + } + } + class ClassfileLoader(val classfile: AbstractFile) extends SymbolLoader with FlagAssigningCompleter { private object classfileParser extends { val symbolTable: SymbolLoaders.this.symbolTable.type = SymbolLoaders.this.symbolTable @@ -293,8 +327,13 @@ abstract class SymbolLoaders { * */ private type SymbolLoadersRefined = SymbolLoaders { val symbolTable: classfileParser.symbolTable.type } + val loaders = SymbolLoaders.this.asInstanceOf[SymbolLoadersRefined] - val classPath = platform.classPath + + override def classFileLookup: util.ClassFileLookup[AbstractFile] = settings.YclasspathImpl.value match { + case ClassPathRepresentationType.Recursive => platform.classPath + case ClassPathRepresentationType.Flat => platform.flatClassPath + } } protected def description = "class file "+ classfile.toString diff --git a/src/compiler/scala/tools/nsc/symtab/classfile/ClassfileParser.scala b/src/compiler/scala/tools/nsc/symtab/classfile/ClassfileParser.scala index 14be8374b9..1abbdb50b0 100644 --- a/src/compiler/scala/tools/nsc/symtab/classfile/ClassfileParser.scala +++ b/src/compiler/scala/tools/nsc/symtab/classfile/ClassfileParser.scala @@ -16,8 +16,7 @@ import scala.annotation.switch import scala.reflect.internal.{ JavaAccFlags } import scala.reflect.internal.pickling.{PickleBuffer, ByteCodecs} import scala.tools.nsc.io.AbstractFile - -import util.ClassPath +import scala.tools.nsc.util.ClassFileLookup /** This abstract class implements a class file parser. * @@ -43,8 +42,8 @@ abstract class ClassfileParser { */ protected def lookupMemberAtTyperPhaseIfPossible(sym: Symbol, name: Name): Symbol - /** The compiler classpath. */ - def classPath: ClassPath[AbstractFile] + /** The way of the class file lookup used by the compiler. */ + def classFileLookup: ClassFileLookup[AbstractFile] import definitions._ import scala.reflect.internal.ClassfileConstants._ @@ -352,7 +351,7 @@ abstract class ClassfileParser { } private def loadClassSymbol(name: Name): Symbol = { - val file = classPath findClassFile ("" +name) getOrElse { + val file = classFileLookup findClassFile name.toString getOrElse { // SI-5593 Scaladoc's current strategy is to visit all packages in search of user code that can be documented // therefore, it will rummage through the classpath triggering errors whenever it encounters package objects // that are not in their correct place (see bug for details) @@ -1047,8 +1046,8 @@ abstract class ClassfileParser { for (entry <- innerClasses.entries) { // create a new class member for immediate inner classes if (entry.outerName == currentClass) { - val file = classPath.findClassFile(entry.externalName.toString) getOrElse { - throw new AssertionError(entry.externalName) + val file = classFileLookup.findClassFile(entry.externalName.toString) getOrElse { + throw new AssertionError(s"Class file for ${entry.externalName} not found") } enterClassAndModule(entry, file) } diff --git a/src/compiler/scala/tools/nsc/symtab/classfile/ICodeReader.scala b/src/compiler/scala/tools/nsc/symtab/classfile/ICodeReader.scala index cbe427775a..bd1fa4e707 100644 --- a/src/compiler/scala/tools/nsc/symtab/classfile/ICodeReader.scala +++ b/src/compiler/scala/tools/nsc/symtab/classfile/ICodeReader.scala @@ -130,7 +130,7 @@ abstract class ICodeReader extends ClassfileParser { log("ICodeReader reading " + cls) val name = cls.javaClassName - classPath.findClassFile(name) match { + classFileLookup.findClassFile(name) match { case Some(classFile) => parse(classFile, cls) case _ => MissingRequirementError.notFound("Could not find bytecode for " + cls) } diff --git a/src/compiler/scala/tools/nsc/transform/AddInterfaces.scala b/src/compiler/scala/tools/nsc/transform/AddInterfaces.scala index 2b7c6cca2c..f786ffb8f3 100644 --- a/src/compiler/scala/tools/nsc/transform/AddInterfaces.scala +++ b/src/compiler/scala/tools/nsc/transform/AddInterfaces.scala @@ -8,6 +8,7 @@ package transform import symtab._ import Flags._ +import scala.tools.nsc.util.ClassPath abstract class AddInterfaces extends InfoTransform { self: Erasure => import global._ // the global environment @@ -67,25 +68,30 @@ abstract class AddInterfaces extends InfoTransform { self: Erasure => val implName = tpnme.implClassName(iface.name) val implFlags = (iface.flags & ~(INTERFACE | lateINTERFACE)) | IMPLCLASS - val impl0 = ( + val impl0 = { if (!inClass) NoSymbol - else iface.owner.info.decl(implName) match { - case NoSymbol => NoSymbol - case implSym => - // Unlink a pre-existing symbol only if the implementation class is - // visible on the compilation classpath. In general this is true under - // -optimise and not otherwise, but the classpath can use arbitrary - // logic so the classpath must be queried. - if (classPath.context.isValidName(implName + ".class")) { - iface.owner.info.decls unlink implSym - NoSymbol - } - else { - log(s"not unlinking $iface's existing implClass ${implSym.name} because it is not on the classpath.") - implSym - } + else { + val typeInfo = iface.owner.info + typeInfo.decl(implName) match { + case NoSymbol => NoSymbol + case implSym => + // Unlink a pre-existing symbol only if the implementation class is + // visible on the compilation classpath. In general this is true under + // -optimise and not otherwise, but the classpath can use arbitrary + // logic so the classpath must be queried. + // TODO this is not taken into account by flat classpath yet + classPath match { + case cp: ClassPath[_] if !cp.context.isValidName(implName + ".class") => + log(s"not unlinking $iface's existing implClass ${implSym.name} because it is not on the classpath.") + implSym + case _ => + typeInfo.decls unlink implSym + NoSymbol + } + } } - ) + } + val impl = impl0 orElse { val impl = iface.owner.newImplClass(implName, iface.pos, implFlags) if (iface.thisSym != iface) { @@ -345,6 +351,7 @@ abstract class AddInterfaces extends InfoTransform { self: Erasure => while (owner != sym && owner != impl) owner = owner.owner; if (owner == impl) This(impl) setPos tree.pos else tree + //TODO what about this commented out code? /* !!! case Super(qual, mix) => val mix1 = mix diff --git a/src/compiler/scala/tools/nsc/transform/Erasure.scala b/src/compiler/scala/tools/nsc/transform/Erasure.scala index b6af19250e..79833e273d 100644 --- a/src/compiler/scala/tools/nsc/transform/Erasure.scala +++ b/src/compiler/scala/tools/nsc/transform/Erasure.scala @@ -185,22 +185,22 @@ abstract class Erasure extends AddInterfaces private def isErasedValueType(tpe: Type) = tpe.isInstanceOf[ErasedValueType] - /* Drop redundant interfaces (ones which are implemented by some other parent) from the immediate parents. + /* Drop redundant types (ones which are implemented by some other parent) from the immediate parents. * This is important on Android because there is otherwise an interface explosion. */ - def minimizeInterfaces(lstIfaces: List[Type]): List[Type] = { - var rest = lstIfaces - var leaves = List.empty[Type] - while(!rest.isEmpty) { + def minimizeParents(parents: List[Type]): List[Type] = { + var rest = parents + var leaves = collection.mutable.ListBuffer.empty[Type] + while(rest.nonEmpty) { val candidate = rest.head val nonLeaf = leaves exists { t => t.typeSymbol isSubClass candidate.typeSymbol } if(!nonLeaf) { - leaves = candidate :: (leaves filterNot { t => candidate.typeSymbol isSubClass t.typeSymbol }) + leaves = leaves filterNot { t => candidate.typeSymbol isSubClass t.typeSymbol } + leaves += candidate } rest = rest.tail } - - leaves.reverse + leaves.toList } @@ -220,7 +220,7 @@ abstract class Erasure extends AddInterfaces case _ => tps } - val minParents = minimizeInterfaces(parents) + val minParents = minimizeParents(parents) val validParents = if (isTraitSignature) // java is unthrilled about seeing interfaces inherit from classes diff --git a/src/compiler/scala/tools/nsc/transform/patmat/Logic.scala b/src/compiler/scala/tools/nsc/transform/patmat/Logic.scala index e6ddf8b758..9f24d5122a 100644 --- a/src/compiler/scala/tools/nsc/transform/patmat/Logic.scala +++ b/src/compiler/scala/tools/nsc/transform/patmat/Logic.scala @@ -16,8 +16,8 @@ trait Logic extends Debugging { import PatternMatchingStats._ private def max(xs: Seq[Int]) = if (xs isEmpty) 0 else xs max - private def alignedColumns(cols: Seq[AnyRef]): Seq[String] = { - def toString(x: AnyRef) = if (x eq null) "" else x.toString + private def alignedColumns(cols: Seq[Any]): Seq[String] = { + def toString(x: Any) = if (x == null) "" else x.toString if (cols.isEmpty || cols.tails.isEmpty) cols map toString else { val colLens = cols map (c => toString(c).length) @@ -32,7 +32,7 @@ trait Logic extends Debugging { } } - def alignAcrossRows(xss: List[List[AnyRef]], sep: String, lineSep: String = "\n"): String = { + def alignAcrossRows(xss: List[List[Any]], sep: String, lineSep: String = "\n"): String = { val maxLen = max(xss map (_.length)) val padded = xss map (xs => xs ++ List.fill(maxLen - xs.length)(null)) padded.transpose.map(alignedColumns).transpose map (_.mkString(sep)) mkString(lineSep) @@ -46,7 +46,7 @@ trait Logic extends Debugging { type Tree class Prop - case class Eq(p: Var, q: Const) extends Prop + final case class Eq(p: Var, q: Const) extends Prop type Const @@ -105,43 +105,146 @@ trait Logic extends Debugging { // would be nice to statically check whether a prop is equational or pure, // but that requires typing relations like And(x: Tx, y: Ty) : (if(Tx == PureProp && Ty == PureProp) PureProp else Prop) - case class And(a: Prop, b: Prop) extends Prop - case class Or(a: Prop, b: Prop) extends Prop - case class Not(a: Prop) extends Prop + final case class And(ops: Set[Prop]) extends Prop + object And { + def apply(ops: Prop*) = new And(ops.toSet) + } + + final case class Or(ops: Set[Prop]) extends Prop + object Or { + def apply(ops: Prop*) = new Or(ops.toSet) + } + + final case class Not(a: Prop) extends Prop case object True extends Prop case object False extends Prop // symbols are propositions - abstract case class Sym(variable: Var, const: Const) extends Prop { + final class Sym private[PropositionalLogic] (val variable: Var, val const: Const) extends Prop { private val id: Int = Sym.nextSymId - override def toString = variable +"="+ const +"#"+ id + override def toString = variable + "=" + const + "#" + id } - class UniqueSym(variable: Var, const: Const) extends Sym(variable, const) + object Sym { private val uniques: HashSet[Sym] = new HashSet("uniques", 512) def apply(variable: Var, const: Const): Sym = { - val newSym = new UniqueSym(variable, const) + val newSym = new Sym(variable, const) (uniques findEntryOrUpdate newSym) } - private def nextSymId = {_symId += 1; _symId}; private var _symId = 0 + def nextSymId = {_symId += 1; _symId}; private var _symId = 0 implicit val SymOrdering: Ordering[Sym] = Ordering.by(_.id) } - def /\(props: Iterable[Prop]) = if (props.isEmpty) True else props.reduceLeft(And(_, _)) - def \/(props: Iterable[Prop]) = if (props.isEmpty) False else props.reduceLeft(Or(_, _)) + def /\(props: Iterable[Prop]) = if (props.isEmpty) True else And(props.toSeq: _*) + def \/(props: Iterable[Prop]) = if (props.isEmpty) False else Or(props.toSeq: _*) + + /** + * Simplifies propositional formula according to the following rules: + * - eliminate double negation (avoids unnecessary Tseitin variables) + * - flatten trees of same connectives (avoids unnecessary Tseitin variables) + * - removes constants and connectives that are in fact constant because of their operands + * - eliminates duplicate operands + * - convert formula into NNF: all sub-expressions have a positive polarity + * which makes them amenable for the subsequent Plaisted transformation + * and increases chances to figure out that the formula is already in CNF + * + * Complexity: DFS over formula tree + * + * See http://www.decision-procedures.org/slides/propositional_logic-2x3.pdf + */ + def simplify(f: Prop): Prop = { + + // limit size to avoid blow up + def hasImpureAtom(ops: Seq[Prop]): Boolean = ops.size < 10 && + ops.combinations(2).exists { + case Seq(a, Not(b)) if a == b => true + case Seq(Not(a), b) if a == b => true + case _ => false + } + + // push negation inside formula + def negationNormalFormNot(p: Prop): Prop = p match { + case And(ops) => Or(ops.map(negationNormalFormNot)) // De'Morgan + case Or(ops) => And(ops.map(negationNormalFormNot)) // De'Morgan + case Not(p) => negationNormalForm(p) + case True => False + case False => True + case s: Sym => Not(s) + } + + def negationNormalForm(p: Prop): Prop = p match { + case And(ops) => And(ops.map(negationNormalForm)) + case Or(ops) => Or(ops.map(negationNormalForm)) + case Not(negated) => negationNormalFormNot(negated) + case True + | False + | (_: Sym) => p + } + + def simplifyProp(p: Prop): Prop = p match { + case And(fv) => + // recurse for nested And (pulls all Ands up) + val ops = fv.map(simplifyProp) - True // ignore `True` + + // build up Set in order to remove duplicates + val opsFlattened = ops.flatMap { + case And(fv) => fv + case f => Set(f) + }.toSeq + + if (hasImpureAtom(opsFlattened) || opsFlattened.contains(False)) { + False + } else { + opsFlattened match { + case Seq() => True + case Seq(f) => f + case ops => And(ops: _*) + } + } + case Or(fv) => + // recurse for nested Or (pulls all Ors up) + val ops = fv.map(simplifyProp) - False // ignore `False` + + val opsFlattened = ops.flatMap { + case Or(fv) => fv + case f => Set(f) + }.toSeq + + if (hasImpureAtom(opsFlattened) || opsFlattened.contains(True)) { + True + } else { + opsFlattened match { + case Seq() => False + case Seq(f) => f + case ops => Or(ops: _*) + } + } + case Not(Not(a)) => + simplify(a) + case Not(p) => + Not(simplify(p)) + case p => + p + } + + val nnf = negationNormalForm(f) + simplifyProp(nnf) + } trait PropTraverser { def apply(x: Prop): Unit = x match { - case And(a, b) => apply(a); apply(b) - case Or(a, b) => apply(a); apply(b) + case And(ops) => ops foreach apply + case Or(ops) => ops foreach apply case Not(a) => apply(a) case Eq(a, b) => applyVar(a); applyConst(b) + case s: Sym => applySymbol(s) case _ => } def applyVar(x: Var): Unit = {} def applyConst(x: Const): Unit = {} + def applySymbol(x: Sym): Unit = {} } def gatherVariables(p: Prop): Set[Var] = { @@ -152,36 +255,27 @@ trait Logic extends Debugging { vars.toSet } + def gatherSymbols(p: Prop): Set[Sym] = { + val syms = new mutable.HashSet[Sym]() + (new PropTraverser { + override def applySymbol(s: Sym) = syms += s + })(p) + syms.toSet + } + trait PropMap { def apply(x: Prop): Prop = x match { // TODO: mapConserve - case And(a, b) => And(apply(a), apply(b)) - case Or(a, b) => Or(apply(a), apply(b)) + case And(ops) => And(ops map apply) + case Or(ops) => Or(ops map apply) case Not(a) => Not(apply(a)) case p => p } } - // to govern how much time we spend analyzing matches for unreachability/exhaustivity - object AnalysisBudget { - private val budgetProp = scala.sys.Prop[String]("scalac.patmat.analysisBudget") - private val budgetOff = "off" - val max: Int = { - val DefaultBudget = 256 - budgetProp.option match { - case Some(`budgetOff`) => - Integer.MAX_VALUE - case Some(x) => - x.toInt - case None => - DefaultBudget - } - } - - abstract class Exception(val advice: String) extends RuntimeException("CNF budget exceeded") - - object exceeded extends Exception( - s"(The analysis required more space than allowed. Please try with scalac -D${budgetProp.key}=${AnalysisBudget.max*2} or -D${budgetProp.key}=${budgetOff}.)") - + // TODO: remove since deprecated + val budgetProp = scala.sys.Prop[String]("scalac.patmat.analysisBudget") + if (budgetProp.isSet) { + reportWarning(s"Please remove -D${budgetProp.key}, it is ignored.") } // convert finite domain propositional logic with subtyping to pure boolean propositional logic @@ -202,7 +296,7 @@ trait Logic extends Debugging { // TODO: for V1 representing x1 and V2 standing for x1.head, encode that // V1 = Nil implies -(V2 = Ci) for all Ci in V2's domain (i.e., it is unassignable) // may throw an AnalysisBudget.Exception - def removeVarEq(props: List[Prop], modelNull: Boolean = false): (Formula, List[Formula]) = { + def removeVarEq(props: List[Prop], modelNull: Boolean = false): (Prop, List[Prop]) = { val start = if (Statistics.canEnable) Statistics.startTimer(patmatAnaVarEq) else null val vars = new mutable.HashSet[Var] @@ -226,10 +320,10 @@ trait Logic extends Debugging { props foreach gatherEqualities.apply if (modelNull) vars foreach (_.registerNull()) - val pure = props map (p => eqFreePropToSolvable(rewriteEqualsToProp(p))) + val pure = props map (p => rewriteEqualsToProp(p)) - val eqAxioms = formulaBuilder - @inline def addAxiom(p: Prop) = addFormula(eqAxioms, eqFreePropToSolvable(p)) + val eqAxioms = mutable.ArrayBuffer[Prop]() + @inline def addAxiom(p: Prop) = eqAxioms += p debug.patmat("removeVarEq vars: "+ vars) vars.foreach { v => @@ -255,49 +349,32 @@ trait Logic extends Debugging { } } - debug.patmat("eqAxioms:\n"+ cnfString(toFormula(eqAxioms))) - debug.patmat("pure:"+ pure.map(p => cnfString(p)).mkString("\n")) + debug.patmat(s"eqAxioms:\n${eqAxioms.mkString("\n")}") + debug.patmat(s"pure:${pure.mkString("\n")}") if (Statistics.canEnable) Statistics.stopTimer(patmatAnaVarEq, start) - (toFormula(eqAxioms), pure) + (And(eqAxioms: _*), pure) } + type Solvable - // an interface that should be suitable for feeding a SAT solver when the time comes - type Formula - type FormulaBuilder - - // creates an empty formula builder to which more formulae can be added - def formulaBuilder: FormulaBuilder - - // val f = formulaBuilder; addFormula(f, f1); ... addFormula(f, fN) - // toFormula(f) == andFormula(f1, andFormula(..., fN)) - def addFormula(buff: FormulaBuilder, f: Formula): Unit - def toFormula(buff: FormulaBuilder): Formula - - // the conjunction of formulae `a` and `b` - def andFormula(a: Formula, b: Formula): Formula - - // equivalent formula to `a`, but simplified in a lightweight way (drop duplicate clauses) - def simplifyFormula(a: Formula): Formula - - // may throw an AnalysisBudget.Exception - def propToSolvable(p: Prop): Formula = { - val (eqAxioms, pure :: Nil) = removeVarEq(List(p), modelNull = false) - andFormula(eqAxioms, pure) + def propToSolvable(p: Prop): Solvable = { + val (eqAxiom, pure :: Nil) = removeVarEq(List(p), modelNull = false) + eqFreePropToSolvable(And(eqAxiom, pure)) } - // may throw an AnalysisBudget.Exception - def eqFreePropToSolvable(p: Prop): Formula - def cnfString(f: Formula): String + def eqFreePropToSolvable(f: Prop): Solvable type Model = Map[Sym, Boolean] val EmptyModel: Model val NoModel: Model - def findModelFor(f: Formula): Model - def findAllModelsFor(f: Formula): List[Model] + final case class Solution(model: Model, unassigned: List[Sym]) + + def findModelFor(solvable: Solvable): Model + + def findAllModelsFor(solvable: Solvable): List[Solution] } } diff --git a/src/compiler/scala/tools/nsc/transform/patmat/MatchAnalysis.scala b/src/compiler/scala/tools/nsc/transform/patmat/MatchAnalysis.scala index 21e90b1d78..d3a5507273 100644 --- a/src/compiler/scala/tools/nsc/transform/patmat/MatchAnalysis.scala +++ b/src/compiler/scala/tools/nsc/transform/patmat/MatchAnalysis.scala @@ -6,6 +6,8 @@ package scala.tools.nsc.transform.patmat +import scala.annotation.tailrec +import scala.collection.immutable.{IndexedSeq, Iterable} import scala.language.postfixOps import scala.collection.mutable import scala.reflect.internal.util.Statistics @@ -266,7 +268,7 @@ trait MatchApproximation extends TreeAndTypeAnalysis with ScalaLogic with MatchT // the type of the binder passed to the first invocation // determines the type of the tree that'll be returned for that binder as of then final def binderToUniqueTree(b: Symbol) = - unique(accumSubst(normalize(CODE.REF(b))), b.tpe) + unique(accumSubst(normalize(gen.mkAttributedStableRef(b))), b.tpe) // note that the sequencing of operations is important: must visit in same order as match execution // binderToUniqueTree uses the type of the first symbol that was encountered as the type for all future binders @@ -397,7 +399,6 @@ trait MatchAnalysis extends MatchApproximation { trait MatchAnalyzer extends MatchApproximator { def uncheckedWarning(pos: Position, msg: String) = currentRun.reporting.uncheckedWarning(pos, msg) - def warn(pos: Position, ex: AnalysisBudget.Exception, kind: String) = uncheckedWarning(pos, s"Cannot check match for $kind.\n${ex.advice}") def reportWarning(message: String) = global.reporter.warning(typer.context.tree.pos, message) // TODO: model dependencies between variables: if V1 corresponds to (x: List[_]) and V2 is (x.hd), V2 cannot be assigned when V1 = null or V1 = Nil @@ -428,49 +429,44 @@ trait MatchAnalysis extends MatchApproximation { val propsCasesOk = approximate(True) map caseWithoutBodyToProp val propsCasesFail = approximate(False) map (t => Not(caseWithoutBodyToProp(t))) - try { - val (eqAxiomsFail, symbolicCasesFail) = removeVarEq(propsCasesFail, modelNull = true) - val (eqAxiomsOk, symbolicCasesOk) = removeVarEq(propsCasesOk, modelNull = true) - val eqAxioms = simplifyFormula(andFormula(eqAxiomsOk, eqAxiomsFail)) // I'm pretty sure eqAxiomsOk == eqAxiomsFail, but not 100% sure. - - val prefix = formulaBuilder - addFormula(prefix, eqAxioms) - - var prefixRest = symbolicCasesFail - var current = symbolicCasesOk - var reachable = true - var caseIndex = 0 - - debug.patmat("reachability, vars:\n"+ ((propsCasesFail flatMap gatherVariables).distinct map (_.describe) mkString ("\n"))) - debug.patmat("equality axioms:\n"+ cnfString(eqAxiomsOk)) - - // invariant (prefixRest.length == current.length) && (prefix.reverse ++ prefixRest == symbolicCasesFail) - // termination: prefixRest.length decreases by 1 - while (prefixRest.nonEmpty && reachable) { - val prefHead = prefixRest.head - caseIndex += 1 - prefixRest = prefixRest.tail - if (prefixRest.isEmpty) reachable = true - else { - addFormula(prefix, prefHead) - current = current.tail - val model = findModelFor(andFormula(current.head, toFormula(prefix))) + val (eqAxiomsFail, symbolicCasesFail) = removeVarEq(propsCasesFail, modelNull = true) + val (eqAxiomsOk, symbolicCasesOk) = removeVarEq(propsCasesOk, modelNull = true) + val eqAxioms = simplify(And(eqAxiomsOk, eqAxiomsFail)) // I'm pretty sure eqAxiomsOk == eqAxiomsFail, but not 100% sure. - // debug.patmat("trying to reach:\n"+ cnfString(current.head) +"\nunder prefix:\n"+ cnfString(prefix)) - // if (NoModel ne model) debug.patmat("reached: "+ modelString(model)) + val prefix = mutable.ArrayBuffer[Prop]() + prefix += eqAxioms - reachable = NoModel ne model - } - } + var prefixRest = symbolicCasesFail + var current = symbolicCasesOk + var reachable = true + var caseIndex = 0 + + debug.patmat("reachability, vars:\n" + ((propsCasesFail flatMap gatherVariables).distinct map (_.describe) mkString ("\n"))) + debug.patmat(s"equality axioms:\n$eqAxiomsOk") + + // invariant (prefixRest.length == current.length) && (prefix.reverse ++ prefixRest == symbolicCasesFail) + // termination: prefixRest.length decreases by 1 + while (prefixRest.nonEmpty && reachable) { + val prefHead = prefixRest.head + caseIndex += 1 + prefixRest = prefixRest.tail + if (prefixRest.isEmpty) reachable = true + else { + prefix += prefHead + current = current.tail + val and = And((current.head +: prefix): _*) + val model = findModelFor(eqFreePropToSolvable(and)) - if (Statistics.canEnable) Statistics.stopTimer(patmatAnaReach, start) + // debug.patmat("trying to reach:\n"+ cnfString(current.head) +"\nunder prefix:\n"+ cnfString(prefix)) + // if (NoModel ne model) debug.patmat("reached: "+ modelString(model)) - if (reachable) None else Some(caseIndex) - } catch { - case ex: AnalysisBudget.Exception => - warn(prevBinder.pos, ex, "unreachability") - None // CNF budget exceeded + reachable = NoModel ne model + } } + + if (Statistics.canEnable) Statistics.stopTimer(patmatAnaReach, start) + + if (reachable) None else Some(caseIndex) } // exhaustivity @@ -518,24 +514,25 @@ trait MatchAnalysis extends MatchApproximation { // debug.patmat("\nvars:\n"+ (vars map (_.describe) mkString ("\n"))) // debug.patmat("\nmatchFails as CNF:\n"+ cnfString(propToSolvable(matchFails))) - try { - // find the models (under which the match fails) - val matchFailModels = findAllModelsFor(propToSolvable(matchFails)) - - val scrutVar = Var(prevBinderTree) - val counterExamples = matchFailModels.flatMap(modelToCounterExample(scrutVar)) - // sorting before pruning is important here in order to - // keep neg/t7020.scala stable - // since e.g. List(_, _) would cover List(1, _) - val pruned = CounterExample.prune(counterExamples.sortBy(_.toString)).map(_.toString) - - if (Statistics.canEnable) Statistics.stopTimer(patmatAnaExhaust, start) - pruned - } catch { - case ex : AnalysisBudget.Exception => - warn(prevBinder.pos, ex, "exhaustivity") - Nil // CNF budget exceeded + // find the models (under which the match fails) + val matchFailModels = findAllModelsFor(propToSolvable(matchFails)) + + val scrutVar = Var(prevBinderTree) + val counterExamples = { + matchFailModels.flatMap { + model => + val varAssignments = expandModel(model) + varAssignments.flatMap(modelToCounterExample(scrutVar) _) + } } + + // sorting before pruning is important here in order to + // keep neg/t7020.scala stable + // since e.g. List(_, _) would cover List(1, _) + val pruned = CounterExample.prune(counterExamples.sortBy(_.toString)).map(_.toString) + + if (Statistics.canEnable) Statistics.stopTimer(patmatAnaExhaust, start) + pruned } } @@ -600,6 +597,8 @@ trait MatchAnalysis extends MatchApproximation { case object WildcardExample extends CounterExample { override def toString = "_" } case object NoExample extends CounterExample { override def toString = "??" } + // returns a mapping from variable to + // equal and notEqual symbols def modelToVarAssignment(model: Model): Map[Var, (Seq[Const], Seq[Const])] = model.toSeq.groupBy{f => f match {case (sym, value) => sym.variable} }.mapValues{ xs => val (trues, falses) = xs.partition(_._2) @@ -613,20 +612,110 @@ trait MatchAnalysis extends MatchApproximation { v +"(="+ v.path +": "+ v.staticTpCheckable +") "+ assignment }.mkString("\n") - // return constructor call when the model is a true counter example - // (the variables don't take into account type information derived from other variables, - // so, naively, you might try to construct a counter example like _ :: Nil(_ :: _, _ :: _), - // since we didn't realize the tail of the outer cons was a Nil) - def modelToCounterExample(scrutVar: Var)(model: Model): Option[CounterExample] = { + /** + * The models we get from the DPLL solver need to be mapped back to counter examples. + * However there's no precalculated mapping model -> counter example. Even worse, + * not every valid model corresponds to a valid counter example. + * The reason is that restricting the valid models further would for example require + * a quadratic number of additional clauses. So to keep the optimistic case fast + * (i.e., all cases are covered in a pattern match), the infeasible counter examples + * are filtered later. + * + * The DPLL procedure keeps the literals that do not contribute to the solution + * unassigned, e.g., for `(a \/ b)` + * only {a = true} or {b = true} is required and the other variable can have any value. + * + * This function does a smart expansion of the model and avoids models that + * have conflicting mappings. + * + * For example for in case of the given set of symbols (taken from `t7020.scala`): + * "V2=2#16" + * "V2=6#19" + * "V2=5#18" + * "V2=4#17" + * "V2=7#20" + * + * One possibility would be to group the symbols by domain but + * this would only work for equality tests and would not be compatible + * with type tests. + * Another observation leads to a much simpler algorithm: + * Only one of these symbols can be set to true, + * since `V2` can at most be equal to one of {2,6,5,4,7}. + */ + def expandModel(solution: Solution): List[Map[Var, (Seq[Const], Seq[Const])]] = { + + val model = solution.model + // x1 = ... // x1.hd = ... // x1.tl = ... // x1.hd.hd = ... // ... val varAssignment = modelToVarAssignment(model) + debug.patmat("var assignment for model " + model + ":\n" + varAssignmentString(varAssignment)) + + // group symbols that assign values to the same variables (i.e., symbols are mutually exclusive) + // (thus the groups are sets of disjoint assignments to variables) + val groupedByVar: Map[Var, List[Sym]] = solution.unassigned.groupBy(_.variable) + + val expanded = for { + (variable, syms) <- groupedByVar.toList + } yield { - debug.patmat("var assignment for model "+ model +":\n"+ varAssignmentString(varAssignment)) + val (equal, notEqual) = varAssignment.getOrElse(variable, Nil -> Nil) + def addVarAssignment(equalTo: List[Const], notEqualTo: List[Const]) = { + Map(variable ->(equal ++ equalTo, notEqual ++ notEqualTo)) + } + + // this assignment is needed in case that + // there exists already an assign + val allNotEqual = addVarAssignment(Nil, syms.map(_.const)) + + // this assignment is conflicting on purpose: + // a list counter example could contain wildcards: e.g. `List(_,_)` + val allEqual = addVarAssignment(syms.map(_.const), Nil) + + if(equal.isEmpty) { + val oneHot = for { + s <- syms + } yield { + addVarAssignment(List(s.const), syms.filterNot(_ == s).map(_.const)) + } + allEqual :: allNotEqual :: oneHot + } else { + allEqual :: allNotEqual :: Nil + } + } + + if (expanded.isEmpty) { + List(varAssignment) + } else { + // we need the cartesian product here, + // since we want to report all missing cases + // (i.e., combinations) + val cartesianProd = expanded.reduceLeft((xs, ys) => + for {map1 <- xs + map2 <- ys} yield { + map1 ++ map2 + }) + + // add expanded variables + // note that we can just use `++` + // since the Maps have disjoint keySets + for { + m <- cartesianProd + } yield { + varAssignment ++ m + } + } + } + + // return constructor call when the model is a true counter example + // (the variables don't take into account type information derived from other variables, + // so, naively, you might try to construct a counter example like _ :: Nil(_ :: _, _ :: _), + // since we didn't realize the tail of the outer cons was a Nil) + def modelToCounterExample(scrutVar: Var)(varAssignment: Map[Var, (Seq[Const], Seq[Const])]): Option[CounterExample] = { // chop a path into a list of symbols def chop(path: Tree): List[Symbol] = path match { case Ident(_) => List(path.symbol) @@ -755,7 +844,7 @@ trait MatchAnalysis extends MatchApproximation { // then we can safely ignore these counter examples since we will eventually encounter // both counter examples separately case _ if inSameDomain => None - + // not a valid counter-example, possibly since we have a definite type but there was a field mismatch // TODO: improve reasoning -- in the mean time, a false negative is better than an annoying false positive case _ => Some(NoExample) @@ -774,12 +863,12 @@ trait MatchAnalysis extends MatchApproximation { } def analyzeCases(prevBinder: Symbol, cases: List[List[TreeMaker]], pt: Type, suppression: Suppression): Unit = { - if (!suppression.unreachable) { + if (!suppression.suppressUnreachable) { unreachableCase(prevBinder, cases, pt) foreach { caseIndex => reportUnreachable(cases(caseIndex).last.pos) } } - if (!suppression.exhaustive) { + if (!suppression.suppressExhaustive) { val counterExamples = exhaustive(prevBinder, cases, pt) if (counterExamples.nonEmpty) reportMissingCases(prevBinder.pos, counterExamples) diff --git a/src/compiler/scala/tools/nsc/transform/patmat/MatchOptimization.scala b/src/compiler/scala/tools/nsc/transform/patmat/MatchOptimization.scala index e9c81f4728..b3aef8a20e 100644 --- a/src/compiler/scala/tools/nsc/transform/patmat/MatchOptimization.scala +++ b/src/compiler/scala/tools/nsc/transform/patmat/MatchOptimization.scala @@ -46,16 +46,16 @@ trait MatchOptimization extends MatchTreeMaking with MatchAnalysis { val cond = test.prop def simplify(c: Prop): Set[Prop] = c match { - case And(a, b) => simplify(a) ++ simplify(b) - case Or(_, _) => Set(False) // TODO: make more precise - case Not(Eq(Var(_), NullConst)) => Set(True) // not worth remembering + case And(ops) => ops.toSet flatMap simplify + case Or(ops) => Set(False) // TODO: make more precise + case Not(Eq(Var(_), NullConst)) => Set(True) // not worth remembering case _ => Set(c) } val conds = simplify(cond) if (conds(False)) false // stop when we encounter a definite "no" or a "not sure" else { - val nonTrivial = conds filterNot (_ == True) + val nonTrivial = conds - True if (nonTrivial nonEmpty) { tested ++= nonTrivial diff --git a/src/compiler/scala/tools/nsc/transform/patmat/MatchTranslation.scala b/src/compiler/scala/tools/nsc/transform/patmat/MatchTranslation.scala index d862805a07..0678ec52e7 100644 --- a/src/compiler/scala/tools/nsc/transform/patmat/MatchTranslation.scala +++ b/src/compiler/scala/tools/nsc/transform/patmat/MatchTranslation.scala @@ -208,7 +208,7 @@ trait MatchTranslation { case _ => (cases, None) } - checkMatchVariablePatterns(nonSyntheticCases) + if (!settings.XnoPatmatAnalysis) checkMatchVariablePatterns(nonSyntheticCases) // we don't transform after uncurry // (that would require more sophistication when generating trees, @@ -248,7 +248,10 @@ trait MatchTranslation { if (caseDefs forall treeInfo.isCatchCase) caseDefs else { val swatches = { // switch-catches - val bindersAndCases = caseDefs map { caseDef => + // SI-7459 must duplicate here as we haven't commited to switch emission, and just figuring out + // if we can ends up mutating `caseDefs` down in the use of `substituteSymbols` in + // `TypedSubstitution#Substitution`. That is called indirectly by `emitTypeSwitch`. + val bindersAndCases = caseDefs.map(_.duplicate) map { caseDef => // generate a fresh symbol for each case, hoping we'll end up emitting a type-switch (we don't have a global scrut there) // if we fail to emit a fine-grained switch, have to do translateCase again with a single scrutSym (TODO: uniformize substitution on treemakers so we can avoid this) val caseScrutSym = freshSym(pos, pureType(ThrowableTpe)) @@ -518,7 +521,7 @@ trait MatchTranslation { // reference the (i-1)th case accessor if it exists, otherwise the (i-1)th tuple component override protected def tupleSel(binder: Symbol)(i: Int): Tree = { val accessors = binder.caseFieldAccessors - if (accessors isDefinedAt (i-1)) REF(binder) DOT accessors(i-1) + if (accessors isDefinedAt (i-1)) gen.mkAttributedStableRef(binder) DOT accessors(i-1) else codegen.tupleSel(binder)(i) // this won't type check for case classes, as they do not inherit ProductN } } @@ -544,10 +547,17 @@ trait MatchTranslation { // wrong when isSeq, and resultInMonad should always be correct since it comes // directly from the extractor's result type val binder = freshSym(pos, pureType(resultInMonad)) + val potentiallyMutableBinders: Set[Symbol] = + if (extractorApply.tpe.typeSymbol.isNonBottomSubClass(OptionClass) && !aligner.isSeq) + Set.empty + else + // Ensures we capture unstable bound variables eagerly. These can arise under name based patmat or by indexing into mutable Seqs. See run t9003.scala + subPatBinders.toSet ExtractorTreeMaker(extractorApply, lengthGuard(binder), binder)( subPatBinders, subPatRefs(binder), + potentiallyMutableBinders, aligner.isBool, checkedLength, patBinderOrCasted, diff --git a/src/compiler/scala/tools/nsc/transform/patmat/MatchTreeMaking.scala b/src/compiler/scala/tools/nsc/transform/patmat/MatchTreeMaking.scala index 3abec521df..971a019e4e 100644 --- a/src/compiler/scala/tools/nsc/transform/patmat/MatchTreeMaking.scala +++ b/src/compiler/scala/tools/nsc/transform/patmat/MatchTreeMaking.scala @@ -21,9 +21,10 @@ trait MatchTreeMaking extends MatchCodeGen with Debugging { import global._ import definitions._ - final case class Suppression(exhaustive: Boolean, unreachable: Boolean) + final case class Suppression(suppressExhaustive: Boolean, suppressUnreachable: Boolean) object Suppression { val NoSuppression = Suppression(false, false) + val FullSuppression = Suppression(true, true) } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -166,8 +167,17 @@ trait MatchTreeMaking extends MatchCodeGen with Debugging { val usedBinders = new mutable.HashSet[Symbol]() // all potentially stored subpat binders val potentiallyStoredBinders = stored.unzip._1.toSet + def ref(sym: Symbol) = + if (potentiallyStoredBinders(sym)) usedBinders += sym // compute intersection of all symbols in the tree `in` and all potentially stored subpat binders - in.foreach(t => if (potentiallyStoredBinders(t.symbol)) usedBinders += t.symbol) + in.foreach { + case tt: TypeTree => + tt.tpe foreach { // SI-7459 e.g. case Prod(t) => new t.u.Foo + case SingleType(_, sym) => ref(sym) + case _ => + } + case t => ref(t.symbol) + } if (usedBinders.isEmpty) in else { @@ -192,13 +202,14 @@ trait MatchTreeMaking extends MatchCodeGen with Debugging { case class ExtractorTreeMaker(extractor: Tree, extraCond: Option[Tree], nextBinder: Symbol)( val subPatBinders: List[Symbol], val subPatRefs: List[Tree], + val potentiallyMutableBinders: Set[Symbol], extractorReturnsBoolean: Boolean, val checkedLength: Option[Int], val prevBinder: Symbol, val ignoredSubPatBinders: Set[Symbol] ) extends FunTreeMaker with PreserveSubPatBinders { - def extraStoredBinders: Set[Symbol] = Set() + def extraStoredBinders: Set[Symbol] = potentiallyMutableBinders debug.patmat(s""" |ExtractorTreeMaker($extractor, $extraCond, $nextBinder) { @@ -541,7 +552,7 @@ trait MatchTreeMaking extends MatchCodeGen with Debugging { debug.patmat("combining cases: "+ (casesNoSubstOnly.map(_.mkString(" >> ")).mkString("{", "\n", "}"))) val (suppression, requireSwitch): (Suppression, Boolean) = - if (settings.XnoPatmatAnalysis) (Suppression.NoSuppression, false) + if (settings.XnoPatmatAnalysis) (Suppression.FullSuppression, false) else scrut match { case Typed(tree, tpt) => val suppressExhaustive = tpt.tpe hasAnnotation UncheckedClass @@ -573,7 +584,7 @@ trait MatchTreeMaking extends MatchCodeGen with Debugging { (Suppression.NoSuppression, false) } - emitSwitch(scrut, scrutSym, casesNoSubstOnly, pt, matchFailGenOverride, suppression.exhaustive).getOrElse{ + emitSwitch(scrut, scrutSym, casesNoSubstOnly, pt, matchFailGenOverride, unchecked = suppression.suppressExhaustive).getOrElse{ if (requireSwitch) reporter.warning(scrut.pos, "could not emit switch for @switch annotated match") if (casesNoSubstOnly nonEmpty) { diff --git a/src/compiler/scala/tools/nsc/transform/patmat/PatternMatching.scala b/src/compiler/scala/tools/nsc/transform/patmat/PatternMatching.scala index ef50e083a1..d35aad964d 100644 --- a/src/compiler/scala/tools/nsc/transform/patmat/PatternMatching.scala +++ b/src/compiler/scala/tools/nsc/transform/patmat/PatternMatching.scala @@ -12,7 +12,7 @@ import scala.language.postfixOps import scala.tools.nsc.transform.TypingTransformers import scala.tools.nsc.transform.Transform import scala.reflect.internal.util.Statistics -import scala.reflect.internal.Types +import scala.reflect.internal.{Mode, Types} import scala.reflect.internal.util.Position /** Translate pattern matching. @@ -198,33 +198,57 @@ trait Interface extends ast.TreeDSL { } class Substitution(val from: List[Symbol], val to: List[Tree]) { - import global.{Transformer, Ident, NoType} + import global.{Transformer, Ident, NoType, TypeTree, SingleType} // We must explicitly type the trees that we replace inside some other tree, since the latter may already have been typed, // and will thus not be retyped. This means we might end up with untyped subtrees inside bigger, typed trees. def apply(tree: Tree): Tree = { // according to -Ystatistics 10% of translateMatch's time is spent in this method... // since about half of the typedSubst's end up being no-ops, the check below shaves off 5% of the time spent in typedSubst - if (!tree.exists { case i@Ident(_) => from contains i.symbol case _ => false}) tree - else (new Transformer { + val toIdents = to.forall(_.isInstanceOf[Ident]) + val containsSym = tree.exists { + case i@Ident(_) => from contains i.symbol + case tt: TypeTree => tt.tpe.exists { + case SingleType(_, sym) => + (from contains sym) && { + if (!toIdents) global.devWarning(s"Unexpected substitution of non-Ident into TypeTree `$tt`, subst= $this") + true + } + case _ => false + } + case _ => false + } + val toSyms = to.map(_.symbol) + object substIdentsForTrees extends Transformer { private def typedIfOrigTyped(to: Tree, origTp: Type): Tree = if (origTp == null || origTp == NoType) to // important: only type when actually substing and when original tree was typed // (don't need to use origTp as the expected type, though, and can't always do this anyway due to unknown type params stemming from polymorphic extractors) else typer.typed(to) + def typedStable(t: Tree) = typer.typed(t.shallowDuplicate, Mode.MonoQualifierModes | Mode.TYPEPATmode) + lazy val toTypes: List[Type] = to map (tree => typedStable(tree).tpe) + override def transform(tree: Tree): Tree = { def subst(from: List[Symbol], to: List[Tree]): Tree = if (from.isEmpty) tree - else if (tree.symbol == from.head) typedIfOrigTyped(to.head.shallowDuplicate.setPos(tree.pos), tree.tpe) + else if (tree.symbol == from.head) typedIfOrigTyped(typedStable(to.head).setPos(tree.pos), tree.tpe) else subst(from.tail, to.tail) - tree match { + val tree1 = tree match { case Ident(_) => subst(from, to) case _ => super.transform(tree) } + tree1.modifyType(_.substituteTypes(from, toTypes)) } - }).transform(tree) + } + if (containsSym) { + if (to.forall(_.isInstanceOf[Ident])) + tree.duplicate.substituteSymbols(from, to.map(_.symbol)) // SI-7459 catches `case t => new t.Foo` + else + substIdentsForTrees.transform(tree) + } + else tree } diff --git a/src/compiler/scala/tools/nsc/transform/patmat/Solving.scala b/src/compiler/scala/tools/nsc/transform/patmat/Solving.scala index 4330781013..27217f0dc2 100644 --- a/src/compiler/scala/tools/nsc/transform/patmat/Solving.scala +++ b/src/compiler/scala/tools/nsc/transform/patmat/Solving.scala @@ -6,252 +6,380 @@ package scala.tools.nsc.transform.patmat -import scala.collection.mutable +import scala.collection.mutable.ArrayBuffer import scala.reflect.internal.util.Statistics import scala.language.postfixOps +import scala.collection.mutable import scala.reflect.internal.util.Collections._ -// naive CNF translation and simple DPLL solver +// a literal is a (possibly negated) variable +class Lit(val v: Int) extends AnyVal { + def unary_- : Lit = Lit(-v) + + def variable: Int = Math.abs(v) + + def positive = v >= 0 + + override def toString(): String = s"Lit#$v" +} + +object Lit { + def apply(v: Int): Lit = new Lit(v) + + implicit val LitOrdering: Ordering[Lit] = Ordering.by(_.v) +} + +/** Solve pattern matcher exhaustivity problem via DPLL. + */ trait Solving extends Logic { + import PatternMatchingStats._ - trait CNF extends PropositionalLogic { - import scala.collection.mutable.ArrayBuffer - type FormulaBuilder = ArrayBuffer[Clause] - def formulaBuilder = ArrayBuffer[Clause]() - def formulaBuilderSized(init: Int) = new ArrayBuffer[Clause](init) - def addFormula(buff: FormulaBuilder, f: Formula): Unit = buff ++= f - def toFormula(buff: FormulaBuilder): Formula = buff - // CNF: a formula is a conjunction of clauses - type Formula = FormulaBuilder - def formula(c: Clause*): Formula = ArrayBuffer(c: _*) + trait CNF extends PropositionalLogic { type Clause = Set[Lit] + // a clause is a disjunction of distinct literals def clause(l: Lit*): Clause = l.toSet - type Lit - def Lit(sym: Sym, pos: Boolean = true): Lit - - def andFormula(a: Formula, b: Formula): Formula = a ++ b - def simplifyFormula(a: Formula): Formula = a.distinct - - private def merge(a: Clause, b: Clause) = a ++ b - - // throws an AnalysisBudget.Exception when the prop results in a CNF that's too big - // TODO: be smarter/more efficient about this (http://lara.epfl.ch/w/sav09:tseitin_s_encoding) - def eqFreePropToSolvable(p: Prop): Formula = { - def negationNormalFormNot(p: Prop, budget: Int): Prop = - if (budget <= 0) throw AnalysisBudget.exceeded - else p match { - case And(a, b) => Or(negationNormalFormNot(a, budget - 1), negationNormalFormNot(b, budget - 1)) - case Or(a, b) => And(negationNormalFormNot(a, budget - 1), negationNormalFormNot(b, budget - 1)) - case Not(p) => negationNormalForm(p, budget - 1) - case True => False - case False => True - case s: Sym => Not(s) + /** Conjunctive normal form (of a Boolean formula). + * A formula in this form is amenable to a SAT solver + * (i.e., solver that decides satisfiability of a formula). + */ + type Cnf = Array[Clause] + + class SymbolMapping(symbols: Set[Sym]) { + val variableForSymbol: Map[Sym, Int] = { + symbols.zipWithIndex.map { + case (sym, i) => sym -> (i + 1) + }.toMap + } + + val symForVar: Map[Int, Sym] = variableForSymbol.map(_.swap) + + val relevantVars: Set[Int] = symForVar.keySet.map(math.abs) + + def lit(sym: Sym): Lit = Lit(variableForSymbol(sym)) + + def size = symbols.size + } + + case class Solvable(cnf: Cnf, symbolMapping: SymbolMapping) + + trait CnfBuilder { + private[this] val buff = ArrayBuffer[Clause]() + + var literalCount: Int + + /** + * @return new Tseitin variable + */ + def newLiteral(): Lit = { + literalCount += 1 + Lit(literalCount) + } + + lazy val constTrue: Lit = { + val constTrue = newLiteral() + addClauseProcessed(clause(constTrue)) + constTrue + } + + def constFalse: Lit = -constTrue + + def isConst(l: Lit): Boolean = l == constTrue || l == constFalse + + def addClauseProcessed(clause: Clause) { + if (clause.nonEmpty) { + buff += clause } + } + + def buildCnf: Array[Clause] = buff.toArray + + } - def negationNormalForm(p: Prop, budget: Int = AnalysisBudget.max): Prop = - if (budget <= 0) throw AnalysisBudget.exceeded - else p match { - case And(a, b) => And(negationNormalForm(a, budget - 1), negationNormalForm(b, budget - 1)) - case Or(a, b) => Or(negationNormalForm(a, budget - 1), negationNormalForm(b, budget - 1)) - case Not(negated) => negationNormalFormNot(negated, budget - 1) - case True - | False - | (_ : Sym) => p + /** Plaisted transformation: used for conversion of a + * propositional formula into conjunctive normal form (CNF) + * (input format for SAT solver). + * A simple conversion into CNF via Shannon expansion would + * also be possible but it's worst-case complexity is exponential + * (in the number of variables) and thus even simple problems + * could become untractable. + * The Plaisted transformation results in an _equisatisfiable_ + * CNF-formula (it generates auxiliary variables) + * but runs with linear complexity. + * The common known Tseitin transformation uses bi-implication, + * whereas the Plaisted transformation uses implication only, thus + * the resulting CNF formula has (on average) only half of the clauses + * of a Tseitin transformation. + * The Plaisted transformation uses the polarities of sub-expressions + * to figure out which part of the bi-implication can be omitted. + * However, if all sub-expressions have positive polarity + * (e.g., after transformation into negation normal form) + * then the conversion is rather simple and the pseudo-normalization + * via NNF increases chances only one side of the bi-implication + * is needed. + */ + class TransformToCnf(symbolMapping: SymbolMapping) extends CnfBuilder { + + // new literals start after formula symbols + var literalCount: Int = symbolMapping.size + + def convertSym(sym: Sym): Lit = symbolMapping.lit(sym) + + def apply(p: Prop): Solvable = { + + def convert(p: Prop): Lit = { + p match { + case And(fv) => + and(fv.map(convert)) + case Or(fv) => + or(fv.map(convert)) + case Not(a) => + not(convert(a)) + case sym: Sym => + convertSym(sym) + case True => + constTrue + case False => + constFalse + case _: Eq => + throw new MatchError(p) + } } - val TrueF = formula() - val FalseF = formula(clause()) - def lit(s: Sym) = formula(clause(Lit(s))) - def negLit(s: Sym) = formula(clause(Lit(s, pos = false))) - - def conjunctiveNormalForm(p: Prop, budget: Int = AnalysisBudget.max): Formula = { - def distribute(a: Formula, b: Formula, budget: Int): Formula = - if (budget <= 0) throw AnalysisBudget.exceeded - else - (a, b) match { - // true \/ _ = true - // _ \/ true = true - case (trueA, trueB) if trueA.size == 0 || trueB.size == 0 => TrueF - // lit \/ lit - case (a, b) if a.size == 1 && b.size == 1 => formula(merge(a(0), b(0))) - // (c1 /\ ... /\ cn) \/ d = ((c1 \/ d) /\ ... /\ (cn \/ d)) - // d \/ (c1 /\ ... /\ cn) = ((d \/ c1) /\ ... /\ (d \/ cn)) - case (cs, ds) => - val (big, small) = if (cs.size > ds.size) (cs, ds) else (ds, cs) - big flatMap (c => distribute(formula(c), small, budget - (big.size*small.size))) - } + def and(bv: Set[Lit]): Lit = { + if (bv.isEmpty) { + // this case can actually happen because `removeVarEq` could add no constraints + constTrue + } else if (bv.size == 1) { + bv.head + } else if (bv.contains(constFalse)) { + constFalse + } else { + // op1 /\ op2 /\ ... /\ opx <==> + // (o -> op1) /\ (o -> op2) ... (o -> opx) /\ (!op1 \/ !op2 \/... \/ !opx \/ o) + // (!o \/ op1) /\ (!o \/ op2) ... (!o \/ opx) /\ (!op1 \/ !op2 \/... \/ !opx \/ o) + val new_bv = bv - constTrue // ignore `True` + val o = newLiteral() // auxiliary Tseitin variable + new_bv.map(op => addClauseProcessed(clause(op, -o))) + o + } + } - if (budget <= 0) throw AnalysisBudget.exceeded - - p match { - case True => TrueF - case False => FalseF - case s: Sym => lit(s) - case Not(s: Sym) => negLit(s) - case And(a, b) => - val cnfA = conjunctiveNormalForm(a, budget - 1) - val cnfB = conjunctiveNormalForm(b, budget - cnfA.size) - cnfA ++ cnfB - case Or(a, b) => - val cnfA = conjunctiveNormalForm(a) - val cnfB = conjunctiveNormalForm(b) - distribute(cnfA, cnfB, budget - (cnfA.size + cnfB.size)) + def or(bv: Set[Lit]): Lit = { + if (bv.isEmpty) { + constFalse + } else if (bv.size == 1) { + bv.head + } else if (bv.contains(constTrue)) { + constTrue + } else { + // op1 \/ op2 \/ ... \/ opx <==> + // (op1 -> o) /\ (op2 -> o) ... (opx -> o) /\ (op1 \/ op2 \/... \/ opx \/ !o) + // (!op1 \/ o) /\ (!op2 \/ o) ... (!opx \/ o) /\ (op1 \/ op2 \/... \/ opx \/ !o) + val new_bv = bv - constFalse // ignore `False` + val o = newLiteral() // auxiliary Tseitin variable + addClauseProcessed(new_bv + (-o)) + o + } } + + // no need for auxiliary variable + def not(a: Lit): Lit = -a + + // add intermediate variable since we want the formula to be SAT! + addClauseProcessed(clause(convert(p))) + + Solvable(buildCnf, symbolMapping) } + } - val start = if (Statistics.canEnable) Statistics.startTimer(patmatCNF) else null - val res = conjunctiveNormalForm(negationNormalForm(p)) + class AlreadyInCNF(symbolMapping: SymbolMapping) { - if (Statistics.canEnable) Statistics.stopTimer(patmatCNF, start) + object ToLiteral { + def unapply(f: Prop): Option[Lit] = f match { + case Not(ToLiteral(lit)) => Some(-lit) + case sym: Sym => Some(symbolMapping.lit(sym)) + case _ => None + } + } - if (Statistics.canEnable) patmatCNFSizes(res.size).value += 1 + object ToDisjunction { + def unapply(f: Prop): Option[Array[Clause]] = f match { + case Or(fv) => + val cl = fv.foldLeft(Option(clause())) { + case (Some(clause), ToLiteral(lit)) => + Some(clause + lit) + case (_, _) => + None + } + cl.map(Array(_)) + case True => Some(Array()) // empty, no clauses needed + case False => Some(Array(clause())) // empty clause can't be satisfied + case ToLiteral(lit) => Some(Array(clause(lit))) + case _ => None + } + } -// debug.patmat("cnf for\n"+ p +"\nis:\n"+cnfString(res)) - res + /** + * Checks if propositional formula is already in CNF + */ + object ToCnf { + def unapply(f: Prop): Option[Solvable] = f match { + case ToDisjunction(clauses) => Some(Solvable(clauses, symbolMapping) ) + case And(fv) => + val clauses = fv.foldLeft(Option(mutable.ArrayBuffer[Clause]())) { + case (Some(cnf), ToDisjunction(clauses)) => + Some(cnf ++= clauses) + case (_, _) => + None + } + clauses.map(c => Solvable(c.toArray, symbolMapping)) + case _ => None + } + } + } + + def eqFreePropToSolvable(p: Prop): Solvable = { + + // collect all variables since after simplification / CNF conversion + // they could have been removed from the formula + val symbolMapping = new SymbolMapping(gatherSymbols(p)) + + val simplified = simplify(p) + val cnfExtractor = new AlreadyInCNF(symbolMapping) + simplified match { + case cnfExtractor.ToCnf(solvable) => + // this is needed because t6942 would generate too many clauses with Tseitin + // already in CNF, just add clauses + solvable + case p => + new TransformToCnf(symbolMapping).apply(p) + } } } // simple solver using DPLL trait Solver extends CNF { - // a literal is a (possibly negated) variable - def Lit(sym: Sym, pos: Boolean = true) = new Lit(sym, pos) - class Lit(val sym: Sym, val pos: Boolean) { - override def toString = if (!pos) "-"+ sym.toString else sym.toString - override def equals(o: Any) = o match { - case o: Lit => (o.sym eq sym) && (o.pos == pos) - case _ => false - } - override def hashCode = sym.hashCode + pos.hashCode + import scala.collection.mutable.ArrayBuffer - def unary_- = Lit(sym, !pos) + def cnfString(f: Array[Clause]): String = { + val lits: Array[List[String]] = f map (_.map(_.toString).toList) + val xss: List[List[String]] = lits toList + val aligned: String = alignAcrossRows(xss, "\\/", " /\\\n") + aligned } - def cnfString(f: Formula) = alignAcrossRows(f map (_.toList) toList, "\\/", " /\\\n") - // adapted from http://lara.epfl.ch/w/sav10:simple_sat_solver (original by Hossein Hojjat) + + // empty set of clauses is trivially satisfied val EmptyModel = Map.empty[Sym, Boolean] + + // no model: originates from the encounter of an empty clause, i.e., + // happens if all variables have been assigned in a way that makes the corresponding literals false + // thus there is no possibility to satisfy that clause, so the whole formula is UNSAT val NoModel: Model = null + // this model contains the auxiliary variables as well + type TseitinModel = Set[Lit] + val EmptyTseitinModel = Set.empty[Lit] + val NoTseitinModel: TseitinModel = null + // returns all solutions, if any (TODO: better infinite recursion backstop -- detect fixpoint??) - def findAllModelsFor(f: Formula): List[Model] = { + def findAllModelsFor(solvable: Solvable): List[Solution] = { + debug.patmat("find all models for\n"+ cnfString(solvable.cnf)) - debug.patmat("find all models for\n"+ cnfString(f)) + // we must take all vars from non simplified formula + // otherwise if we get `T` as formula, we don't expand the variables + // that are not in the formula... + val relevantVars: Set[Int] = solvable.symbolMapping.relevantVars - val vars: Set[Sym] = f.flatMap(_ collect {case l: Lit => l.sym}).toSet // debug.patmat("vars "+ vars) // the negation of a model -(S1=True/False /\ ... /\ SN=True/False) = clause(S1=False/True, ...., SN=False/True) - def negateModel(m: Model) = clause(m.toSeq.map{ case (sym, pos) => Lit(sym, !pos) } : _*) - - /** - * The DPLL procedure only returns a minimal mapping from literal to value - * such that the CNF formula is satisfied. - * E.g. for: - * `(a \/ b)` - * The DPLL procedure will find either {a = true} or {b = true} - * as solution. - * - * The expansion step will amend both solutions with the unassigned variable - * i.e., {a = true} will be expanded to {a = true, b = true} and {a = true, b = false}. - */ - def expandUnassigned(unassigned: List[Sym], model: Model): List[Model] = { - // the number of solutions is doubled for every unassigned variable - val expandedModels = 1 << unassigned.size - var current = mutable.ArrayBuffer[Model]() - var next = mutable.ArrayBuffer[Model]() - current.sizeHint(expandedModels) - next.sizeHint(expandedModels) - - current += model - - // we use double buffering: - // read from `current` and create a two models for each model in `next` - for { - s <- unassigned - } { - for { - model <- current - } { - def force(l: Lit) = model + (l.sym -> l.pos) - - next += force(Lit(s, pos = true)) - next += force(Lit(s, pos = false)) - } - - val tmp = current - current = next - next = tmp - - next.clear() - } - - current.toList + // (i.e. the blocking clause - used for ALL-SAT) + def negateModel(m: TseitinModel) = { + // filter out auxiliary Tseitin variables + val relevantLits = m.filter(l => relevantVars.contains(l.variable)) + relevantLits.map(lit => -lit) } - def findAllModels(f: Formula, - models: List[Model], - recursionDepthAllowed: Int = global.settings.YpatmatExhaustdepth.value): List[Model]= + final case class TseitinSolution(model: TseitinModel, unassigned: List[Int]) { + def projectToSolution(symForVar: Map[Int, Sym]) = Solution(projectToModel(model, symForVar), unassigned map symForVar) + } + def findAllModels(clauses: Array[Clause], + models: List[TseitinSolution], + recursionDepthAllowed: Int = global.settings.YpatmatExhaustdepth.value): List[TseitinSolution]= if (recursionDepthAllowed == 0) { val maxDPLLdepth = global.settings.YpatmatExhaustdepth.value reportWarning("(Exhaustivity analysis reached max recursion depth, not all missing cases are reported. " + s"Please try with scalac -Ypatmat-exhaust-depth ${maxDPLLdepth * 2} or -Ypatmat-exhaust-depth off.)") models } else { - val model = findModelFor(f) + debug.patmat("find all models for\n" + cnfString(clauses)) + val model = findTseitinModelFor(clauses) // if we found a solution, conjunct the formula with the model's negation and recurse - if (model ne NoModel) { - val unassigned = (vars -- model.keySet).toList + if (model ne NoTseitinModel) { + // note that we should not expand the auxiliary variables (from Tseitin transformation) + // since they are existentially quantified in the final solution + val unassigned: List[Int] = (relevantVars -- model.map(lit => lit.variable)).toList debug.patmat("unassigned "+ unassigned +" in "+ model) - val forced = expandUnassigned(unassigned, model) - debug.patmat("forced "+ forced) + val solution = TseitinSolution(model, unassigned) val negated = negateModel(model) - findAllModels(f :+ negated, forced ++ models, recursionDepthAllowed - 1) + findAllModels(clauses :+ negated, solution :: models, recursionDepthAllowed - 1) } else models } - findAllModels(f, Nil) + val tseitinSolutions = findAllModels(solvable.cnf, Nil) + tseitinSolutions.map(_.projectToSolution(solvable.symbolMapping.symForVar)) } - private def withLit(res: Model, l: Lit): Model = if (res eq NoModel) NoModel else res + (l.sym -> l.pos) - private def dropUnit(f: Formula, unitLit: Lit): Formula = { + private def withLit(res: TseitinModel, l: Lit): TseitinModel = { + if (res eq NoTseitinModel) NoTseitinModel else res + l + } + + /** Drop trivially true clauses, simplify others by dropping negation of `unitLit`. + * + * Disjunctions that contain the literal we're making true in the returned model are trivially true. + * Clauses can be simplified by dropping the negation of the literal we're making true + * (since False \/ X == X) + */ + private def dropUnit(clauses: Array[Clause], unitLit: Lit): Array[Clause] = { val negated = -unitLit - // drop entire clauses that are trivially true - // (i.e., disjunctions that contain the literal we're making true in the returned model), - // and simplify clauses by dropping the negation of the literal we're making true - // (since False \/ X == X) - val dropped = formulaBuilderSized(f.size) - for { - clause <- f - if !(clause contains unitLit) - } dropped += (clause - negated) - dropped + val simplified = new ArrayBuffer[Clause](clauses.size) + clauses foreach { + case trivial if trivial contains unitLit => // drop + case clause => simplified += clause - negated + } + simplified.toArray + } + + def findModelFor(solvable: Solvable): Model = { + projectToModel(findTseitinModelFor(solvable.cnf), solvable.symbolMapping.symForVar) } - def findModelFor(f: Formula): Model = { - @inline def orElse(a: Model, b: => Model) = if (a ne NoModel) a else b + def findTseitinModelFor(clauses: Array[Clause]): TseitinModel = { + @inline def orElse(a: TseitinModel, b: => TseitinModel) = if (a ne NoTseitinModel) a else b - debug.patmat("DPLL\n"+ cnfString(f)) + debug.patmat(s"DPLL\n${cnfString(clauses)}") val start = if (Statistics.canEnable) Statistics.startTimer(patmatAnaDPLL) else null - val satisfiableWithModel: Model = - if (f isEmpty) EmptyModel - else if(f exists (_.isEmpty)) NoModel - else f.find(_.size == 1) match { + val satisfiableWithModel: TseitinModel = + if (clauses isEmpty) EmptyTseitinModel + else if (clauses exists (_.isEmpty)) NoTseitinModel + else clauses.find(_.size == 1) match { case Some(unitClause) => val unitLit = unitClause.head - // debug.patmat("unit: "+ unitLit) - withLit(findModelFor(dropUnit(f, unitLit)), unitLit) + withLit(findTseitinModelFor(dropUnit(clauses, unitLit)), unitLit) case _ => // partition symbols according to whether they appear in positive and/or negative literals - val pos = new mutable.HashSet[Sym]() - val neg = new mutable.HashSet[Sym]() - mforeach(f)(lit => if (lit.pos) pos += lit.sym else neg += lit.sym) + val pos = new mutable.HashSet[Int]() + val neg = new mutable.HashSet[Int]() + mforeach(clauses)(lit => if (lit.positive) pos += lit.variable else neg += lit.variable) // appearing in both positive and negative val impures = pos intersect neg @@ -259,23 +387,38 @@ trait Solving extends Logic { val pures = (pos ++ neg) -- impures if (pures nonEmpty) { - val pureSym = pures.head + val pureVar = pures.head // turn it back into a literal // (since equality on literals is in terms of equality // of the underlying symbol and its positivity, simply construct a new Lit) - val pureLit = Lit(pureSym, pos(pureSym)) + val pureLit = Lit(if (neg(pureVar)) -pureVar else pureVar) // debug.patmat("pure: "+ pureLit +" pures: "+ pures +" impures: "+ impures) - val simplified = f.filterNot(_.contains(pureLit)) - withLit(findModelFor(simplified), pureLit) + val simplified = clauses.filterNot(_.contains(pureLit)) + withLit(findTseitinModelFor(simplified), pureLit) } else { - val split = f.head.head + val split = clauses.head.head // debug.patmat("split: "+ split) - orElse(findModelFor(f :+ clause(split)), findModelFor(f :+ clause(-split))) + orElse(findTseitinModelFor(clauses :+ clause(split)), findTseitinModelFor(clauses :+ clause(-split))) } } if (Statistics.canEnable) Statistics.stopTimer(patmatAnaDPLL, start) satisfiableWithModel } + + private def projectToModel(model: TseitinModel, symForVar: Map[Int, Sym]): Model = + if (model == NoTseitinModel) NoModel + else if (model == EmptyTseitinModel) EmptyModel + else { + val mappedModels = model.toList collect { + case lit if symForVar isDefinedAt lit.variable => (symForVar(lit.variable), lit.positive) + } + if (mappedModels.isEmpty) { + // could get an empty model if mappedModels is a constant like `True` + EmptyModel + } else { + mappedModels.toMap + } + } } } diff --git a/src/compiler/scala/tools/nsc/typechecker/Checkable.scala b/src/compiler/scala/tools/nsc/typechecker/Checkable.scala index 3a77cab919..fc632e0d0d 100644 --- a/src/compiler/scala/tools/nsc/typechecker/Checkable.scala +++ b/src/compiler/scala/tools/nsc/typechecker/Checkable.scala @@ -11,12 +11,28 @@ import scala.language.postfixOps /** On pattern matcher checkability: * + * The spec says that case _: List[Int] should be always issue + * an unchecked warning: + * + * > Types which are not of one of the forms described above are + * > also accepted as type patterns. However, such type patterns + * > will be translated to their erasure (§3.7). The Scala compiler + * > will issue an “unchecked” warning for these patterns to flag + * > the possible loss of type-safety. + * + * But the implementation goes a little further to omit warnings + * based on the static type of the scrutinee. As a trivial example: + * + * def foo(s: Seq[Int]) = s match { case _: List[Int] => } + * + * need not issue this warning. + * * Consider a pattern match of this form: (x: X) match { case _: P => } * * There are four possibilities to consider: * [P1] X will always conform to P * [P2] x will never conform to P - * [P3] X <: P if some runtime test is true + * [P3] X will conform to P if some runtime test is true * [P4] X cannot be checked against P * * The first two cases correspond to those when there is enough @@ -28,6 +44,11 @@ import scala.language.postfixOps * which is essentially the intersection of X and |P|, where |P| is * the erasure of P. If XR <: P, then no warning is emitted. * + * We evaluate "X with conform to P" by checking `X <: P_wild, where + * P_wild is the result of substituting wildcard types in place of + * pattern type variables. This is intentionally stricter than + * (X matchesPattern P), see SI-8597 for motivating test cases. + * * Examples of how this info is put to use: * sealed trait A[T] ; class B[T] extends A[T] * def f(x: B[Int]) = x match { case _: A[Int] if true => } @@ -100,7 +121,7 @@ trait Checkable { private def typeArgsInTopLevelType(tp: Type): List[Type] = { val tps = tp match { case RefinedType(parents, _) => parents flatMap typeArgsInTopLevelType - case TypeRef(_, ArrayClass, arg :: Nil) => typeArgsInTopLevelType(arg) + case TypeRef(_, ArrayClass, arg :: Nil) => if (arg.typeSymbol.isAbstractType) arg :: Nil else typeArgsInTopLevelType(arg) case TypeRef(pre, sym, args) => typeArgsInTopLevelType(pre) ++ args case ExistentialType(tparams, underlying) => tparams.map(_.tpe) ++ typeArgsInTopLevelType(underlying) case _ => Nil @@ -108,14 +129,31 @@ trait Checkable { tps filterNot isUnwarnableTypeArg } + private def scrutConformsToPatternType(scrut: Type, pattTp: Type): Boolean = { + def typeVarToWildcard(tp: Type) = { + // The need for typeSymbolDirect is demonstrated in neg/t8597b.scala + if (tp.typeSymbolDirect.isPatternTypeVariable) WildcardType else tp + } + val pattTpWild = pattTp.map(typeVarToWildcard) + scrut <:< pattTpWild + } + private class CheckabilityChecker(val X: Type, val P: Type) { def Xsym = X.typeSymbol def Psym = P.typeSymbol - def XR = if (Xsym == AnyClass) classExistentialType(Psym) else propagateKnownTypes(X, Psym) + def PErased = { + P match { + case erasure.GenericArray(n, core) => existentialAbstraction(core.typeSymbol :: Nil, P) + case _ => existentialAbstraction(Psym.typeParams, Psym.tpe_*) + } + } + def XR = if (Xsym == AnyClass) PErased else propagateKnownTypes(X, Psym) + + // sadly the spec says (new java.lang.Boolean(true)).isInstanceOf[scala.Boolean] - def P1 = X matchesPattern P + def P1 = scrutConformsToPatternType(X, P) def P2 = !Psym.isPrimitiveValueClass && isNeverSubType(X, P) - def P3 = isNonRefinementClassType(P) && (XR matchesPattern P) + def P3 = isNonRefinementClassType(P) && scrutConformsToPatternType(XR, P) def P4 = !(P1 || P2 || P3) def summaryString = f""" diff --git a/src/compiler/scala/tools/nsc/typechecker/Contexts.scala b/src/compiler/scala/tools/nsc/typechecker/Contexts.scala index b13f9e94cc..a7d0d32c6f 100644 --- a/src/compiler/scala/tools/nsc/typechecker/Contexts.scala +++ b/src/compiler/scala/tools/nsc/typechecker/Contexts.scala @@ -104,7 +104,7 @@ trait Contexts { self: Analyzer => // there must be a scala.xml package when xml literals were parsed in this unit if (unit.hasXml && ScalaXmlPackage == NoSymbol) - reporter.error(unit.firstXmlPos, "To compile XML syntax, the scala.xml package must be on the classpath.\nPlease see http://docs.scala-lang.org/overviews/core/scala-2.11.html#scala-xml.") + reporter.error(unit.firstXmlPos, "To compile XML syntax, the scala.xml package must be on the classpath.\nPlease see https://github.com/scala/scala-xml for details.") // scala-xml needs `scala.xml.TopScope` to be in scope globally as `$scope` // We detect `scala-xml` by looking for `scala.xml.TopScope` and @@ -480,7 +480,8 @@ trait Contexts { self: Analyzer => // SI-8245 `isLazy` need to skip lazy getters to ensure `return` binds to the right place c.enclMethod = if (isDefDef && !owner.isLazy) c else enclMethod - if (tree != outer.tree) c(TypeConstructorAllowed) = false + if (tree != outer.tree) + c(TypeConstructorAllowed) = false registerContext(c.asInstanceOf[analyzer.Context]) debuglog("[context] ++ " + c.unit + " / " + tree.summaryString) diff --git a/src/compiler/scala/tools/nsc/typechecker/Namers.scala b/src/compiler/scala/tools/nsc/typechecker/Namers.scala index a1de5e303b..0bb94be636 100644 --- a/src/compiler/scala/tools/nsc/typechecker/Namers.scala +++ b/src/compiler/scala/tools/nsc/typechecker/Namers.scala @@ -584,7 +584,7 @@ trait Namers extends MethodSynthesis { // more than one hidden name, the second will not be warned. // So it is the position of the actual hidden name. // - // Note: java imports have precence over definitions in the same package + // Note: java imports have precedence over definitions in the same package // so don't warn for them. There is a corresponding special treatment // in the shadowing rules in typedIdent to (SI-7232). In any case, // we shouldn't be emitting warnings for .java source files. diff --git a/src/compiler/scala/tools/nsc/typechecker/Typers.scala b/src/compiler/scala/tools/nsc/typechecker/Typers.scala index 3f2911e2ef..5719a9e358 100644 --- a/src/compiler/scala/tools/nsc/typechecker/Typers.scala +++ b/src/compiler/scala/tools/nsc/typechecker/Typers.scala @@ -3792,8 +3792,12 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper case TypeRef(pre, sym, args) => if (sym.isAliasType && containsLocal(tp) && (tp.dealias ne tp)) apply(tp.dealias) else { - if (pre.isVolatile) - InferTypeWithVolatileTypeSelectionError(tree, pre) + if (pre.isVolatile) pre match { + case SingleType(_, sym) if sym.isSynthetic && isPastTyper => + debuglog(s"ignoring volatility of prefix in pattern matcher generated inferred type: $tp") // See pos/t7459c.scala + case _ => + InferTypeWithVolatileTypeSelectionError(tree, pre) + } mapOver(tp) } case _ => @@ -5223,7 +5227,11 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper def typedExistentialTypeTree(tree: ExistentialTypeTree) = { val tree1 = typerWithLocalContext(context.makeNewScope(tree, context.owner)){ - _.typedExistentialTypeTree(tree, mode) + typer => + if (context.inTypeConstructorAllowed) + typer.context.withinTypeConstructorAllowed(typer.typedExistentialTypeTree(tree, mode)) + else + typer.typedExistentialTypeTree(tree, mode) } checkExistentialsFeature(tree1.pos, tree1.tpe, "the existential type") tree1 diff --git a/src/compiler/scala/tools/nsc/util/ClassFileLookup.scala b/src/compiler/scala/tools/nsc/util/ClassFileLookup.scala new file mode 100644 index 0000000000..4451651229 --- /dev/null +++ b/src/compiler/scala/tools/nsc/util/ClassFileLookup.scala @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2014 Contributor. All rights reserved. + */ +package scala.tools.nsc.util + +import scala.tools.nsc.io.AbstractFile +import java.net.URL + +/** + * Simple interface that allows us to abstract over how class file lookup is performed + * in different classpath representations. + */ +// TODO at the end, after the possible removal of the old classpath representation, this class shouldn't be generic +// T should be just changed to AbstractFile +trait ClassFileLookup[T] { + def findClassFile(name: String): Option[AbstractFile] + + /** + * It returns both classes from class file and source files (as our base ClassRepresentation). + * So note that it's not so strictly related to findClassFile. + */ + def findClass(name: String): Option[ClassRepresentation[T]] + + /** + * A sequence of URLs representing this classpath. + */ + def asURLs: Seq[URL] + + /** The whole classpath in the form of one String. + */ + def asClassPathString: String + + // for compatibility purposes + @deprecated("Use asClassPathString instead of this one", "2.11.5") + def asClasspathString: String = asClassPathString + + /** The whole sourcepath in the form of one String. + */ + def asSourcePathString: String +} + +/** + * Represents classes which can be loaded with a ClassfileLoader and/or SourcefileLoader. + */ +// TODO at the end, after the possible removal of the old classpath implementation, this class shouldn't be generic +// T should be just changed to AbstractFile +trait ClassRepresentation[T] { + def binary: Option[T] + def source: Option[AbstractFile] + + def name: String +} + +object ClassRepresentation { + def unapply[T](classRep: ClassRepresentation[T]): Option[(Option[T], Option[AbstractFile])] = + Some((classRep.binary, classRep.source)) +} diff --git a/src/compiler/scala/tools/nsc/util/ClassPath.scala b/src/compiler/scala/tools/nsc/util/ClassPath.scala index e78dee5eee..8d4d07759f 100644 --- a/src/compiler/scala/tools/nsc/util/ClassPath.scala +++ b/src/compiler/scala/tools/nsc/util/ClassPath.scala @@ -7,16 +7,18 @@ package scala.tools.nsc package util +import io.{ AbstractFile, Directory, File, Jar } +import java.net.MalformedURLException import java.net.URL +import java.util.regex.PatternSyntaxException import scala.collection.{ mutable, immutable } -import io.{ File, Directory, Path, Jar, AbstractFile } import scala.reflect.internal.util.StringOps.splitWhere -import Jar.isJarOrZip +import scala.tools.nsc.classpath.FileUtils + import File.pathSeparator -import scala.collection.convert.WrapAsScala.enumerationAsScalaIterator -import java.net.MalformedURLException -import java.util.regex.PatternSyntaxException -import scala.reflect.runtime.ReflectionUtils +import FileUtils.endsClass +import FileUtils.endsScalaOrJava +import Jar.isJarOrZip /** <p> * This module provides star expansion of '-classpath' option arguments, behaves the same as @@ -89,7 +91,7 @@ object ClassPath { /** A class modeling aspects of a ClassPath which should be * propagated to any classpaths it creates. */ - abstract class ClassPathContext[T] { + abstract class ClassPathContext[T] extends classpath.ClassPathFactory[ClassPath[T]] { /** A filter which can be used to exclude entities from the classpath * based on their name. */ @@ -99,75 +101,47 @@ object ClassPath { */ def validClassFile(name: String) = endsClass(name) && isValidName(name) def validPackage(name: String) = (name != "META-INF") && (name != "") && (name.charAt(0) != '.') - def validSourceFile(name: String) = endsScala(name) || endsJava(name) + def validSourceFile(name: String) = endsScalaOrJava(name) /** From the representation to its identifier. */ def toBinaryName(rep: T): String - /** Create a new classpath based on the abstract file. - */ - def newClassPath(file: AbstractFile): ClassPath[T] - - /** Creators for sub classpaths which preserve this context. - */ def sourcesInPath(path: String): List[ClassPath[T]] = for (file <- expandPath(path, expandStar = false) ; dir <- Option(AbstractFile getDirectory file)) yield new SourcePath[T](dir, this) - - def contentsOfDirsInPath(path: String): List[ClassPath[T]] = - for (dir <- expandPath(path, expandStar = false) ; name <- expandDir(dir) ; entry <- Option(AbstractFile getDirectory name)) yield - newClassPath(entry) - - def classesInExpandedPath(path: String): IndexedSeq[ClassPath[T]] = - classesInPathImpl(path, expand = true).toIndexedSeq - - def classesInPath(path: String) = classesInPathImpl(path, expand = false) - - // Internal - private def classesInPathImpl(path: String, expand: Boolean) = - for (file <- expandPath(path, expand) ; dir <- Option(AbstractFile getDirectory file)) yield - newClassPath(dir) - - def classesInManifest(used: Boolean) = - if (used) for (url <- manifests) yield newClassPath(AbstractFile getResources url) else Nil } - def manifests = Thread.currentThread().getContextClassLoader().getResources("META-INF/MANIFEST.MF").filter(_.getProtocol() == "jar").toList + def manifests: List[java.net.URL] = { + import scala.collection.convert.WrapAsScala.enumerationAsScalaIterator + Thread.currentThread().getContextClassLoader() + .getResources("META-INF/MANIFEST.MF") + .filter(_.getProtocol == "jar").toList + } class JavaContext extends ClassPathContext[AbstractFile] { def toBinaryName(rep: AbstractFile) = { val name = rep.name assert(endsClass(name), name) - name.substring(0, name.length - 6) + FileUtils.stripClassExtension(name) } + def newClassPath(dir: AbstractFile) = new DirectoryClassPath(dir, this) } object DefaultJavaContext extends JavaContext - private def endsClass(s: String) = s.length > 6 && s.substring(s.length - 6) == ".class" - private def endsScala(s: String) = s.length > 6 && s.substring(s.length - 6) == ".scala" - private def endsJava(s: String) = s.length > 5 && s.substring(s.length - 5) == ".java" - /** From the source file to its identifier. */ - def toSourceName(f: AbstractFile): String = { - val name = f.name - - if (endsScala(name)) name.substring(0, name.length - 6) - else if (endsJava(name)) name.substring(0, name.length - 5) - else throw new FatalError("Unexpected source file ending: " + name) - } + def toSourceName(f: AbstractFile): String = FileUtils.stripSourceExtension(f.name) } + import ClassPath._ /** * Represents a package which contains classes and other packages */ -abstract class ClassPath[T] { - type AnyClassRep = ClassPath[T]#ClassRep - +abstract class ClassPath[T] extends ClassFileLookup[T] { /** * The short name of the package (without prefix) */ @@ -179,21 +153,13 @@ abstract class ClassPath[T] { */ def origin: Option[String] = None - /** A list of URLs representing this classpath. - */ - def asURLs: List[URL] - - /** The whole classpath in the form of one String. - */ - def asClasspathString: String - /** Info which should be propagated to any sub-classpaths. */ def context: ClassPathContext[T] /** Lists of entities. */ - def classes: IndexedSeq[AnyClassRep] + def classes: IndexedSeq[ClassRepresentation[T]] def packages: IndexedSeq[ClassPath[T]] def sourcepaths: IndexedSeq[AbstractFile] @@ -217,7 +183,7 @@ abstract class ClassPath[T] { /** * Represents classes which can be loaded with a ClassfileLoader and/or SourcefileLoader. */ - case class ClassRep(binary: Option[T], source: Option[AbstractFile]) { + case class ClassRep(binary: Option[T], source: Option[AbstractFile]) extends ClassRepresentation[T] { def name: String = binary match { case Some(x) => context.toBinaryName(x) case _ => @@ -236,25 +202,27 @@ abstract class ClassPath[T] { * Find a ClassRep given a class name of the form "package.subpackage.ClassName". * Does not support nested classes on .NET */ - def findClass(name: String): Option[AnyClassRep] = + override def findClass(name: String): Option[ClassRepresentation[T]] = splitWhere(name, _ == '.', doDropIndex = true) match { case Some((pkg, rest)) => val rep = packages find (_.name == pkg) flatMap (_ findClass rest) rep map { - case x: ClassRep => x + case x: ClassRepresentation[T] => x case x => throw new FatalError("Unexpected ClassRep '%s' found searching for name '%s'".format(x, name)) } case _ => classes find (_.name == name) } - def findClassFile(name: String): Option[AbstractFile] = + override def findClassFile(name: String): Option[AbstractFile] = findClass(name) match { - case Some(ClassRep(Some(x: AbstractFile), _)) => Some(x) + case Some(ClassRepresentation(Some(x: AbstractFile), _)) => Some(x) case _ => None } - def sortString = join(split(asClasspathString).sorted: _*) + override def asSourcePathString: String = sourcepaths.mkString(pathSeparator) + + def sortString = join(split(asClassPathString).sorted: _*) override def equals(that: Any) = that match { case x: ClassPath[_] => this.sortString == x.sortString case _ => false @@ -266,10 +234,12 @@ abstract class ClassPath[T] { * A Classpath containing source files */ class SourcePath[T](dir: AbstractFile, val context: ClassPathContext[T]) extends ClassPath[T] { + import FileUtils.AbstractFileOps + def name = dir.name override def origin = dir.underlyingSource map (_.path) - def asURLs = if (dir.file == null) Nil else List(dir.toURL) - def asClasspathString = dir.path + def asURLs = dir.toURLs() + def asClassPathString = dir.path val sourcepaths: IndexedSeq[AbstractFile] = IndexedSeq(dir) private def traverse() = { @@ -292,10 +262,12 @@ class SourcePath[T](dir: AbstractFile, val context: ClassPathContext[T]) extends * A directory (or a .jar file) containing classfiles and packages */ class DirectoryClassPath(val dir: AbstractFile, val context: ClassPathContext[AbstractFile]) extends ClassPath[AbstractFile] { + import FileUtils.AbstractFileOps + def name = dir.name override def origin = dir.underlyingSource map (_.path) - def asURLs = if (dir.file == null) List(new URL(name)) else List(dir.toURL) - def asClasspathString = dir.path + def asURLs = dir.toURLs(default = Seq(new URL(name))) + def asClassPathString = dir.path val sourcepaths: IndexedSeq[AbstractFile] = IndexedSeq() // calculates (packages, classes) in one traversal. @@ -342,6 +314,7 @@ class MergedClassPath[T]( override val entries: IndexedSeq[ClassPath[T]], val context: ClassPathContext[T]) extends ClassPath[T] { + def this(entries: TraversableOnce[ClassPath[T]], context: ClassPathContext[T]) = this(entries.toIndexedSeq, context) @@ -350,12 +323,12 @@ extends ClassPath[T] { lazy val sourcepaths: IndexedSeq[AbstractFile] = entries flatMap (_.sourcepaths) override def origin = Some(entries map (x => x.origin getOrElse x.name) mkString ("Merged(", ", ", ")")) - override def asClasspathString: String = join(entries map (_.asClasspathString) : _*) + override def asClassPathString: String = join(entries map (_.asClassPathString) : _*) - lazy val classes: IndexedSeq[AnyClassRep] = { + lazy val classes: IndexedSeq[ClassRepresentation[T]] = { var count = 0 val indices = mutable.HashMap[String, Int]() - val cls = new mutable.ArrayBuffer[AnyClassRep](1024) + val cls = new mutable.ArrayBuffer[ClassRepresentation[T]](1024) for (e <- entries; c <- e.classes) { val name = c.name @@ -364,9 +337,9 @@ extends ClassPath[T] { val existing = cls(idx) if (existing.binary.isEmpty && c.binary.isDefined) - cls(idx) = existing.copy(binary = c.binary) + cls(idx) = ClassRep(binary = c.binary, source = existing.source) if (existing.source.isEmpty && c.source.isDefined) - cls(idx) = existing.copy(source = c.source) + cls(idx) = ClassRep(binary = existing.binary, source = c.source) } else { indices(name) = count @@ -404,10 +377,12 @@ extends ClassPath[T] { } new MergedClassPath[T](newEntries, context) } + def show() { println("ClassPath %s has %d entries and results in:\n".format(name, entries.size)) - asClasspathString split ':' foreach (x => println(" " + x)) + asClassPathString split ':' foreach (x => println(" " + x)) } + override def toString() = "merged classpath "+ entries.mkString("(", "\n", ")") } diff --git a/src/compiler/scala/tools/reflect/ReflectMain.scala b/src/compiler/scala/tools/reflect/ReflectMain.scala index 3ae21b6b98..8d8418945a 100644 --- a/src/compiler/scala/tools/reflect/ReflectMain.scala +++ b/src/compiler/scala/tools/reflect/ReflectMain.scala @@ -1,17 +1,17 @@ package scala.tools package reflect +import scala.reflect.internal.util.ScalaClassLoader import scala.tools.nsc.Driver import scala.tools.nsc.Global import scala.tools.nsc.Settings -import scala.tools.nsc.util.ScalaClassLoader -import scala.tools.util.PathResolver +import scala.tools.util.PathResolverFactory object ReflectMain extends Driver { private def classloaderFromSettings(settings: Settings) = { - val classpath = new PathResolver(settings).result - ScalaClassLoader.fromURLs(classpath.asURLs, getClass.getClassLoader) + val classPathURLs = PathResolverFactory.create(settings).resultAsURLs + ScalaClassLoader.fromURLs(classPathURLs, getClass.getClassLoader) } override def newCompiler(): Global = new ReflectGlobal(settings, reporter, classloaderFromSettings(settings)) diff --git a/src/compiler/scala/tools/util/PathResolver.scala b/src/compiler/scala/tools/util/PathResolver.scala index 5526660509..8e5b1e0a5c 100644 --- a/src/compiler/scala/tools/util/PathResolver.scala +++ b/src/compiler/scala/tools/util/PathResolver.scala @@ -7,14 +7,17 @@ package scala package tools package util +import java.net.URL import scala.tools.reflect.WrappedProperties.AccessControl -import scala.tools.nsc.{ Settings } -import scala.tools.nsc.util.{ ClassPath, JavaClassPath } +import scala.tools.nsc.Settings +import scala.tools.nsc.util.{ ClassFileLookup, ClassPath, JavaClassPath } import scala.reflect.io.{ File, Directory, Path, AbstractFile } import scala.reflect.runtime.ReflectionUtils import ClassPath.{ JavaContext, DefaultJavaContext, join, split } import PartialFunction.condOpt import scala.language.postfixOps +import scala.tools.nsc.classpath.{ AggregateFlatClassPath, ClassPathFactory, FlatClassPath, FlatClassPathFactory } +import scala.tools.nsc.settings.ClassPathRepresentationType // Loosely based on the draft specification at: // https://wiki.scala-lang.org/display/SIW/Classpath @@ -48,9 +51,8 @@ object PathResolver { /** Values found solely by inspecting environment or property variables. */ object Environment { - private def searchForBootClasspath = ( + private def searchForBootClasspath = systemProperties find (_._1 endsWith ".boot.class.path") map (_._2) getOrElse "" - ) /** Environment variables which java pays attention to so it * seems we do as well. @@ -104,7 +106,7 @@ object PathResolver { else if (scalaLibAsDir.isDirectory) scalaLibAsDir.path else "" - // XXX It must be time for someone to figure out what all these things + // TODO It must be time for someone to figure out what all these things // are intended to do. This is disabled here because it was causing all // the scala jars to end up on the classpath twice: one on the boot // classpath as set up by the runner (or regular classpath under -nobootcp) @@ -170,39 +172,48 @@ object PathResolver { !ReflectionUtils.scalacShouldntLoadClassfile(name) } - // called from scalap + @deprecated("This method is no longer used be scalap and will be deleted", "2.11.5") def fromPathString(path: String, context: JavaContext = DefaultJavaContext): JavaClassPath = { val s = new Settings() s.classpath.value = path - new PathResolver(s, context) result + new PathResolver(s, context).result } /** With no arguments, show the interesting values in Environment and Defaults. * If there are arguments, show those in Calculated as if those options had been * given to a scala runner. */ - def main(args: Array[String]): Unit = { + def main(args: Array[String]): Unit = if (args.isEmpty) { println(Environment) println(Defaults) - } - else { + } else { val settings = new Settings() val rest = settings.processArguments(args.toList, processAll = false)._2 - val pr = new PathResolver(settings) - println(" COMMAND: 'scala %s'".format(args.mkString(" "))) + val pr = PathResolverFactory.create(settings) + println("COMMAND: 'scala %s'".format(args.mkString(" "))) println("RESIDUAL: 'scala %s'\n".format(rest.mkString(" "))) - pr.result.show() + + pr.result match { + case cp: JavaClassPath => + cp.show() + case cp: AggregateFlatClassPath => + println(s"ClassPath has ${cp.aggregates.size} entries and results in:\n${cp.asClassPathStrings}") + } } - } } -class PathResolver(settings: Settings, context: JavaContext) { - import PathResolver.{ Defaults, Environment, AsLines, MkLines, ppcp } +trait PathResolverResult { + def result: ClassFileLookup[AbstractFile] - def this(settings: Settings) = this(settings, - if (settings.YnoLoadImplClass) PathResolver.NoImplClassJavaContext - else DefaultJavaContext) + def resultAsURLs: Seq[URL] = result.asURLs +} + +abstract class PathResolverBase[BaseClassPathType <: ClassFileLookup[AbstractFile], ResultClassPathType <: BaseClassPathType] +(settings: Settings, classPathFactory: ClassPathFactory[BaseClassPathType]) + extends PathResolverResult { + + import PathResolver.{ AsLines, Defaults, ppcp } private def cmdLineOrElse(name: String, alt: String) = { (commandLineFor(name) match { @@ -232,6 +243,7 @@ class PathResolver(settings: Settings, context: JavaContext) { def javaUserClassPath = if (useJavaClassPath) Defaults.javaUserClassPath else "" def scalaBootClassPath = cmdLineOrElse("bootclasspath", Defaults.scalaBootClassPath) def scalaExtDirs = cmdLineOrElse("extdirs", Defaults.scalaExtDirs) + /** Scaladoc doesn't need any bootstrapping, otherwise will create errors such as: * [scaladoc] ../scala-trunk/src/reflect/scala/reflect/macros/Reifiers.scala:89: error: object api is not a member of package reflect * [scaladoc] case class ReificationException(val pos: reflect.api.PositionApi, val msg: String) extends Throwable(msg) @@ -250,16 +262,14 @@ class PathResolver(settings: Settings, context: JavaContext) { * - Otherwise, if CLASSPATH is set, it is that * - If neither of those, then "." is used. */ - def userClassPath = ( - if (!settings.classpath.isDefault) - settings.classpath.value + def userClassPath = + if (!settings.classpath.isDefault) settings.classpath.value else sys.env.getOrElse("CLASSPATH", ".") - ) - import context._ + import classPathFactory._ // Assemble the elements! - def basis = List[Traversable[ClassPath[AbstractFile]]]( + def basis = List[Traversable[BaseClassPathType]]( classesInPath(javaBootClassPath), // 1. The Java bootstrap class path. contentsOfDirsInPath(javaExtDirs), // 2. The Java extension class path. classesInExpandedPath(javaUserClassPath), // 3. The Java application class path. @@ -278,7 +288,7 @@ class PathResolver(settings: Settings, context: JavaContext) { | javaBootClassPath = ${ppcp(javaBootClassPath)} | javaExtDirs = ${ppcp(javaExtDirs)} | javaUserClassPath = ${ppcp(javaUserClassPath)} - | useJavaClassPath = $useJavaClassPath + | useJavaClassPath = $useJavaClassPath | scalaBootClassPath = ${ppcp(scalaBootClassPath)} | scalaExtDirs = ${ppcp(scalaExtDirs)} | userClassPath = ${ppcp(userClassPath)} @@ -288,8 +298,10 @@ class PathResolver(settings: Settings, context: JavaContext) { def containers = Calculated.containers - lazy val result = { - val cp = new JavaClassPath(containers.toIndexedSeq, context) + import PathResolver.MkLines + + def result: ResultClassPathType = { + val cp = computeResult() if (settings.Ylogcp) { Console print f"Classpath built from ${settings.toConciseString} %n" Console print s"Defaults: ${PathResolver.Defaults}" @@ -301,5 +313,37 @@ class PathResolver(settings: Settings, context: JavaContext) { cp } - def asURLs = result.asURLs + @deprecated("Use resultAsURLs instead of this one", "2.11.5") + def asURLs: List[URL] = resultAsURLs.toList + + protected def computeResult(): ResultClassPathType +} + +class PathResolver(settings: Settings, context: JavaContext) + extends PathResolverBase[ClassPath[AbstractFile], JavaClassPath](settings, context) { + + def this(settings: Settings) = + this(settings, + if (settings.YnoLoadImplClass) PathResolver.NoImplClassJavaContext + else DefaultJavaContext) + + override protected def computeResult(): JavaClassPath = + new JavaClassPath(containers.toIndexedSeq, context) +} + +class FlatClassPathResolver(settings: Settings, flatClassPathFactory: ClassPathFactory[FlatClassPath]) + extends PathResolverBase[FlatClassPath, AggregateFlatClassPath](settings, flatClassPathFactory) { + + def this(settings: Settings) = this(settings, new FlatClassPathFactory(settings)) + + override protected def computeResult(): AggregateFlatClassPath = AggregateFlatClassPath(containers.toIndexedSeq) +} + +object PathResolverFactory { + + def create(settings: Settings): PathResolverResult = + settings.YclasspathImpl.value match { + case ClassPathRepresentationType.Flat => new FlatClassPathResolver(settings) + case ClassPathRepresentationType.Recursive => new PathResolver(settings) + } } diff --git a/src/compiler/scala/tools/util/SocketServer.scala b/src/compiler/scala/tools/util/SocketServer.scala index 1d39a59cf4..7858bf0658 100644 --- a/src/compiler/scala/tools/util/SocketServer.scala +++ b/src/compiler/scala/tools/util/SocketServer.scala @@ -28,12 +28,12 @@ trait CompileOutputCommon { * @author Martin Odersky * @version 1.0 */ -abstract class SocketServer extends CompileOutputCommon { +abstract class SocketServer(fixPort: Int = 0) extends CompileOutputCommon { def shutdown: Boolean def session(): Unit def timeout(): Unit = () // called after a timeout is detected for subclasses to cleanup // a hook for subclasses - protected def createServerSocket(): ServerSocket = new ServerSocket(0) + protected def createServerSocket(): ServerSocket = new ServerSocket(fixPort) var in: BufferedReader = _ var out: PrintWriter = _ |