From 0573a7779b3e0ee6558ac07b0e4bc740708e4824 Mon Sep 17 00:00:00 2001 From: Tom Grigg Date: Fri, 18 Mar 2022 13:50:21 -0700 Subject: [PATCH 1/6] refactor class loader extensions --- .../src/dotty/tools/MainGenericRunner.scala | 4 +- .../src/dotty/tools/repl/ReplDriver.scala | 2 +- .../src/dotty/tools/runner/ObjectRunner.scala | 4 +- .../dotty/tools/runner/ScalaClassLoader.scala | 96 +++++++++---------- tests/run-with-compiler/i14541.scala | 4 +- 5 files changed, 50 insertions(+), 60 deletions(-) diff --git a/compiler/src/dotty/tools/MainGenericRunner.scala b/compiler/src/dotty/tools/MainGenericRunner.scala index dca7178c52f5..7c984c8fda64 100644 --- a/compiler/src/dotty/tools/MainGenericRunner.scala +++ b/compiler/src/dotty/tools/MainGenericRunner.scala @@ -170,7 +170,7 @@ object MainGenericRunner { val newSettings = if arg.startsWith("-") then settings else settings.withPossibleEntryPaths(arg).withModeShouldBePossibleRun process(tail, newSettings.withResidualArgs(arg)) end process - + def main(args: Array[String]): Unit = val scalaOpts = envOrNone("SCALA_OPTS").toArray.flatMap(_.split(" ")).filter(_.nonEmpty) val allArgs = scalaOpts ++ args @@ -193,7 +193,7 @@ object MainGenericRunner { case ExecuteMode.PossibleRun => val newClasspath = (settings.classPath :+ ".").flatMap(_.split(classpathSeparator).filter(_.nonEmpty)).map(File(_).toURI.toURL) - import dotty.tools.runner.RichClassLoader._ + import dotty.tools.runner.ClassLoaderOps._ val newClassLoader = ScalaClassLoader.fromURLsParallelCapable(newClasspath) val targetToRun = settings.possibleEntryPaths.to(LazyList).find { entryPath => newClassLoader.tryToLoadClass(entryPath).orElse { diff --git a/compiler/src/dotty/tools/repl/ReplDriver.scala b/compiler/src/dotty/tools/repl/ReplDriver.scala index 184e1c0817fb..1093304496fa 100644 --- a/compiler/src/dotty/tools/repl/ReplDriver.scala +++ b/compiler/src/dotty/tools/repl/ReplDriver.scala @@ -31,7 +31,7 @@ import dotty.tools.dotc.util.{SourceFile, SourcePosition} import dotty.tools.dotc.{CompilationUnit, Driver} import dotty.tools.dotc.config.CompilerCommand import dotty.tools.io._ -import dotty.tools.runner.ScalaClassLoader.* +import dotty.tools.runner.ClassLoaderOps.* import org.jline.reader._ import scala.annotation.tailrec diff --git a/compiler/src/dotty/tools/runner/ObjectRunner.scala b/compiler/src/dotty/tools/runner/ObjectRunner.scala index cb8f9d791dfa..04e4d2e8faaa 100644 --- a/compiler/src/dotty/tools/runner/ObjectRunner.scala +++ b/compiler/src/dotty/tools/runner/ObjectRunner.scala @@ -19,8 +19,8 @@ trait CommonRunner { * @throws java.lang.reflect.InvocationTargetException */ def run(urls: Seq[URL], objectName: String, arguments: Seq[String]): Unit = { - import RichClassLoader._ - ScalaClassLoader.fromURLsParallelCapable(urls).run(objectName, arguments) + import ClassLoaderOps._ + ScalaClassLoader.fromURLsParallelCapable(urls).runMain(objectName, arguments) } /** Catches any non-fatal exception thrown by run (in the case of InvocationTargetException, diff --git a/compiler/src/dotty/tools/runner/ScalaClassLoader.scala b/compiler/src/dotty/tools/runner/ScalaClassLoader.scala index bbc028b217c1..03819f36d69f 100644 --- a/compiler/src/dotty/tools/runner/ScalaClassLoader.scala +++ b/compiler/src/dotty/tools/runner/ScalaClassLoader.scala @@ -5,62 +5,61 @@ import scala.language.unsafeNulls import java.lang.ClassLoader import java.lang.invoke.{MethodHandles, MethodType} -import java.lang.reflect.Modifier -import java.net.{ URL, URLClassLoader } -import java.lang.reflect.{ InvocationTargetException, UndeclaredThrowableException } +import java.lang.reflect.{Modifier, InvocationTargetException, UndeclaredThrowableException} +import java.net.{URL, URLClassLoader} import scala.annotation.internal.sharable import scala.annotation.tailrec import scala.util.control.Exception.catching -final class RichClassLoader(private val self: ClassLoader) extends AnyVal { - /** Execute an action with this classloader as context classloader. */ - private def asContext[T](action: => T): T = ScalaClassLoader.asContext(self)(action) +object ClassLoaderOps: + private def setContext(cl: ClassLoader) = Thread.currentThread.setContextClassLoader(cl) - /** Load and link a class with this classloader */ - def tryToLoadClass[T <: AnyRef](path: String): Option[Class[T]] = tryClass(path, initialize = false) - - /** Load, link and initialize a class with this classloader */ - def tryToInitializeClass[T <: AnyRef](path: String): Option[Class[T]] = tryClass(path, initialize = true) + extension (self: ClassLoader) + /** Execute an action with this classloader as context classloader. */ + def asContext[T](action: => T): T = + val saved = Thread.currentThread.getContextClassLoader + try + setContext(self) + action + finally setContext(saved) - private def tryClass[T <: AnyRef](path: String, initialize: Boolean): Option[Class[T]] = - catching(classOf[ClassNotFoundException], classOf[SecurityException]) opt - Class.forName(path, initialize, self).asInstanceOf[Class[T]] + /** Load and link a class with this classloader */ + def tryToLoadClass[T <: AnyRef](path: String): Option[Class[T]] = tryClass(path, initialize = false) - /** Run the main method of a class to be loaded by this classloader */ - def run(objectName: String, arguments: Seq[String]): Unit = { - val clsToRun = tryToInitializeClass(objectName).getOrElse(throw new ClassNotFoundException(objectName)) - val method = clsToRun.getMethod("main", classOf[Array[String]]) - if !Modifier.isStatic(method.getModifiers) then - throw new NoSuchMethodException(s"$objectName.main is not static") - try asContext(method.invoke(null, Array(arguments.toArray: AnyRef): _*)) - catch unwrapHandler({ case ex => throw ex }) - } + /** Load, link and initialize a class with this classloader */ + def tryToInitializeClass[T <: AnyRef](path: String): Option[Class[T]] = tryClass(path, initialize = true) - @tailrec private def unwrapThrowable(x: Throwable): Throwable = x match { - case _: InvocationTargetException | // thrown by reflectively invoked method or constructor - _: ExceptionInInitializerError | // thrown when running a static initializer (e.g. a scala module constructor) - _: UndeclaredThrowableException | // invocation on a proxy instance if its invocation handler's `invoke` throws an exception - _: ClassNotFoundException | // no definition for a class instantiated by name - _: NoClassDefFoundError // the definition existed when the executing class was compiled, but can no longer be found - if x.getCause != null => - unwrapThrowable(x.getCause) - case _ => x - } + private def tryClass[T <: AnyRef](path: String, initialize: Boolean): Option[Class[T]] = + catching(classOf[ClassNotFoundException], classOf[SecurityException]) opt + Class.forName(path, initialize, self).asInstanceOf[Class[T]] - // Transforms an exception handler into one which will only receive the unwrapped - // exceptions (for the values of wrap covered in unwrapThrowable.) - private def unwrapHandler[T](pf: PartialFunction[Throwable, T]): PartialFunction[Throwable, T] = - pf.compose({ case ex => unwrapThrowable(ex) }) -} + /** Run the main method of a class to be loaded by this classloader */ + def runMain(objectName: String, arguments: Seq[String]): Unit = + val clsToRun = tryToInitializeClass(objectName).getOrElse(throw ClassNotFoundException(objectName)) + val method = clsToRun.getMethod("main", classOf[Array[String]]) + if !Modifier.isStatic(method.getModifiers) then + throw NoSuchMethodException(s"$objectName.main is not static") + try asContext(method.invoke(null, Array(arguments.toArray: AnyRef)*)) + catch unwrapHandler({ case ex => throw ex }) -object RichClassLoader { - implicit def wrapClassLoader(loader: ClassLoader): RichClassLoader = new RichClassLoader(loader) -} + @tailrec private def unwrapThrowable(x: Throwable): Throwable = x match + case _: InvocationTargetException | // thrown by reflectively invoked method or constructor + _: ExceptionInInitializerError | // thrown when running a static initializer (e.g. a scala module constructor) + _: UndeclaredThrowableException | // invocation on a proxy instance if its invocation handler's `invoke` throws an exception + _: ClassNotFoundException | // no definition for a class instantiated by name + _: NoClassDefFoundError // the definition existed when the executing class was compiled, but can no longer be found + if x.getCause != null => + unwrapThrowable(x.getCause) + case _ => x -object ScalaClassLoader { - def setContext(cl: ClassLoader) = Thread.currentThread.setContextClassLoader(cl) + // Transforms an exception handler into one which will only receive the unwrapped + // exceptions (for the values of wrap covered in unwrapThrowable.) + private def unwrapHandler[T](pf: PartialFunction[Throwable, T]): PartialFunction[Throwable, T] = + pf.compose({ case ex => unwrapThrowable(ex) }) +end ClassLoaderOps +object ScalaClassLoader: def fromURLsParallelCapable(urls: Seq[URL], parent: ClassLoader | Null = null): URLClassLoader = new URLClassLoader(urls.toArray, if parent == null then bootClassLoader else parent) @@ -70,13 +69,4 @@ object ScalaClassLoader { MethodHandles.lookup().findStatic(classOf[ClassLoader], "getPlatformClassLoader", MethodType.methodType(classOf[ClassLoader])).invoke().asInstanceOf[ClassLoader] catch case _: Throwable => null else null - - extension (classLoader: ClassLoader) - /** Execute an action with this classloader as context classloader. */ - def asContext[T](action: => T): T = - val saved = Thread.currentThread.getContextClassLoader - try - setContext(classLoader) - action - finally setContext(saved) -} +end ScalaClassLoader diff --git a/tests/run-with-compiler/i14541.scala b/tests/run-with-compiler/i14541.scala index 0fdfb89674d5..7d3452b875b7 100644 --- a/tests/run-with-compiler/i14541.scala +++ b/tests/run-with-compiler/i14541.scala @@ -1,10 +1,10 @@ // test argument processing and "execution mode" in runner object Test: - import dotty.tools.runner.RichClassLoader.* + import dotty.tools.runner.ClassLoaderOps.* val classpath = dotty.tools.dotc.util.ClasspathFromClassloader(getClass.getClassLoader) def main(args: Array[String]): Unit = - getClass.getClassLoader.run("echo", List("hello", "raw", "world")) + getClass.getClassLoader.runMain("echo", List("hello", "raw", "world")) // caution: uses "SCALA_OPTS" dotty.tools.MainGenericRunner.main(Array("--class-path", classpath, "echo", "hello", "run", "world")) From 858741464a15aa8233477246aa01be49fbb87882 Mon Sep 17 00:00:00 2001 From: Tom Grigg Date: Thu, 17 Mar 2022 19:46:07 -0700 Subject: [PATCH 2/6] Add REPL disassembler framework This commit only provides a framework to support bytecode disassembly from within the REPL, it does not supply any concrete implementations using any particular disassembler -- those will follow in subsequent commits. Adapted from the Scala 2 :javap implementation, which was written by Paul Phillips and Som Snytt / A. P. Marki --- .../src/dotty/tools/repl/Disassembler.scala | 273 ++++++++++++++++++ .../src/dotty/tools/repl/ReplDriver.scala | 220 ++++++++++---- .../src/dotty/tools/repl/ReplStrings.scala | 12 + .../dotty/tools/runner/ScalaClassLoader.scala | 10 + .../dotty/tools/repl/DisassemblerTests.scala | 250 ++++++++++++++++ 5 files changed, 705 insertions(+), 60 deletions(-) create mode 100644 compiler/src/dotty/tools/repl/Disassembler.scala create mode 100644 compiler/src/dotty/tools/repl/ReplStrings.scala create mode 100644 compiler/test/dotty/tools/repl/DisassemblerTests.scala diff --git a/compiler/src/dotty/tools/repl/Disassembler.scala b/compiler/src/dotty/tools/repl/Disassembler.scala new file mode 100644 index 000000000000..7298210ed170 --- /dev/null +++ b/compiler/src/dotty/tools/repl/Disassembler.scala @@ -0,0 +1,273 @@ +package dotty.tools +package repl + +import scala.annotation.internal.sharable +import scala.util.{Failure, Success, Try} +import scala.util.matching.Regex + +import dotc.core.StdNames.* +import DisResult.* + +/** Abstract representation of a disassembler. + * The high-level disassembly process is as follows: + * 1. parse the arguments to disassembly command + * 2. map input targets to class bytes via DisassemblyClass + * 3. select a DisassemblyTool implementation and run it to generate disassembly text + * 4. perform any post-processing/filtering of the output text + */ +abstract class Disassembler: + import Disassembler.* + + /** Run the disassembly tool with the supplied options, in the context of a DisassemblerRepl */ + def apply(opts: DisassemblerOptions)(using DisassemblerRepl): List[DisResult] + + /** A list of help strings for the flags supported by this disassembler. + * Each entry is of the form: "-flag" -> "help text" + */ + def helps: List[(String, String)] + + /** Formatted help text for this disassembler. */ + def helpText: String = helps.map((name, help) => f"${name}%-12.12s${help}%s%n").mkString + + /** The post-processing filters to be applied to the text results of this disassembler, + * based on the options in effect and the disassembly target. The filtering of REPL + * naming artifacts is implemented here and enabled by the special `-filter` flag; + * subclasses may provide additional filters as appropriate. + */ + def filters(target: String, opts: DisassemblerOptions): List[String => String] = + if opts.filterReplNames then filterReplNames :: Nil else Nil + + /** Combined chain of filters for post-processing disassembly output. */ + final def outputFilter(target: String, opts: DisassemblerOptions): String => String = + filters(target, opts) match + case Nil => identity + case fs => Function.chain(fs) + +object Disassembler: + @sharable private val ReplWrapperName = ( + Regex.quote(str.REPL_SESSION_LINE) + raw"\d+" + Regex.quote("$") + "?" + ).r + + /** A filter to remove REPL wrapper names from the output. */ + def filterReplNames(in: String): String = ReplWrapperName.replaceAllIn(in, "") + + /** Utility method to perform line-by-line filtering based on a predicate. */ + def filteredLines(text: String, pred: String => Boolean): String = + val bldr = StringBuilder() + text.linesIterator.foreach(line => + if pred(line) then + bldr.append(line).append('\n') + ) + bldr.toString + + /** Extract any member name from a disassembly target + * e.g. Foo#bar. Foo# yields zero-length member part. + */ + def splitHashMember(s: String): Option[String] = + s.lastIndexOf('#') match + case -1 => None + case i => Some(s.drop(i + 1)) +end Disassembler + +/** The result of a disassembly command. */ +enum DisResult: + case DisError(message: String | Null) + case DisSuccess(target: String, output: String) + +/** The REPL context used for disassembly. */ +case class DisassemblerRepl(driver: ReplDriver, state: State): + def classLoader: ClassLoader = driver.replClassLoader()(using state.context) + def mostRecentEntry: Seq[String] = driver.disassemblyTargetsLastWrapper(state) + +final case class DisassemblerOptions(flags: Seq[String], targets: Seq[String], filterReplNames: Boolean) + +/** A generic option parser, the available options are taken from `helps` */ +abstract class DisassemblerOptionParser(helps: List[(String, String)]): + def defaultToolOptions: List[String] + + /** Parse the arguments to the disassembly tool. + * Option args start with "-", except that "-" itself denotes the last REPL result. + */ + def parse(args: Seq[String])(using repl: DisassemblerRepl): DisassemblerOptions = + val (options0, targets0) = args.partition(s => s.startsWith("-") && s.length > 1) + val (options, filterReplNames) = + val (opts, flag) = toolArgs(options0) + (if opts.isEmpty then defaultToolOptions else opts, flag) + + // "-" may expand into multiple targets (e.g. if multiple type defs in a single wrapper) + val targets = targets0.flatMap { + case "-" => repl.mostRecentEntry + case s => Seq(s) + } + DisassemblerOptions(options, targets, filterReplNames) + + // split tool options from REPL's -filter flag, also take prefixes of flag names + private def toolArgs(args: Seq[String]): (Seq[String], Boolean) = + val (opts, rest) = args.flatMap(massage).partition(_ != "-filter") + (opts, rest.nonEmpty) + + private def massage(arg: String): Seq[String] = + require(arg.startsWith("-")) + // arg matches opt "-foo/-f" if prefix of -foo or exactly -f + val r = """(-[^/]*)(?:/(-.))?""".r + + def maybe(opt: String, s: String): Option[String] = opt match + // disambiguate by preferring short form + case r(lf, sf) if s == sf => Some(sf) + case r(lf, sf) if lf startsWith s => Some(lf) + case _ => None + + def candidates(s: String) = helps.map(h => maybe(h._1, s)).flatten + + // one candidate or one single-char candidate + def uniqueOf(maybes: Seq[String]) = + def single(s: String) = s.length == 2 + if maybes.length == 1 then maybes + else if maybes.count(single) == 1 then maybes.filter(single) + else Nil + + // each optchar must decode to exactly one option + def unpacked(s: String): Try[Seq[String]] = + val ones = s.drop(1).map(c => + val maybes = uniqueOf(candidates(s"-$c")) + if maybes.length == 1 then Some(maybes.head) else None + ) + Try(ones) filter (_ forall (_.isDefined)) map (_.flatten) + + val res = uniqueOf(candidates(arg)) + if res.nonEmpty then res + else unpacked(arg).getOrElse(Seq("-help")) // or else someone needs help + end massage +end DisassemblerOptionParser + +/** A tool to perform disassembly of class bytes. */ +abstract class DisassemblyTool: + import DisassemblyTool.* + def apply(options: Seq[String])(inputs: Seq[Input]): List[DisResult] + +object DisassemblyTool: + /** The input to a disassembly tool. + * + * @param target The disassembly target as given by the user. + * @param actual The class name or file name where the target data was found. + * @param data The class bytes to be disassembled. + */ + case class Input(target: String, actual: String, data: Try[Array[Byte]]) + +/** A provider of the bytes to be disassembled. + * + * Handles translation of an input path to a (possible empty) array of bytes + * from the specified classloader, where the input path may be: + * - a class name (possibly qualified) + * - the name of a type or term symbol in scope + * - the filesystem path to a .class file + * + * The REPL uses an in-memory classloader, so depending on the target of the + * disassembly, the bytes under examination may not exist on disk. + */ +class DisassemblyClass(loader: ClassLoader)(using repl: DisassemblerRepl): + import DisassemblyClass.* + import DisassemblyTool.* + import dotty.tools.io.File + import dotty.tools.runner.ClassLoaderOps.* + import java.io.FileNotFoundException + + /** Associate the requested path with a possibly failed or empty array of bytes. */ + def bytes(path: String): Input = + bytesFor(path) match + case Success((actual, bytes)) => Input(path, actual, Success(bytes)) + case Failure(ex) => Input(path, path, Failure(ex)) + + /** Find bytes. Handle "Foo#bar" (by ignoring member), "#bar" (by taking "bar"). + * @return the path to use for filtering, and the byte array + */ + private def bytesFor(path: String) = + import scala.language.unsafeNulls // lampepfl/dotty#14672 + Try { + path match + case HashSplit(prefix, _) if prefix != null => prefix + case HashSplit(_, member) if member != null => member + case s => s + }.flatMap(findBytes) + + // data paired with actual path where it was found + private def findBytes(path: String) = tryFile(path) orElse tryClass(path) + + /** Assume the string is a path and try to find the classfile it represents. */ + private def tryFile(path: String): Try[(String, Array[Byte])] = + Try(File(path.asClassResource)) + .filter(_.exists) + .map(f => (path, f.toByteArray())) + + /** Assume the string is a fully qualified class name and try to + * find the class object it represents. + * There are other symbols of interest, too: + * - a definition that is wrapped in an enclosing class + * - a synthetic that is not in scope but its associated class is + */ + private def tryClass(path: String): Try[(String, Array[Byte])] = + given State = repl.state + + def loadable(name: String) = loader.resourceable(name) + + // if path has an interior dollar, take it as a synthetic + // if the prefix up to the dollar is a symbol in scope, + // result is the translated prefix + suffix + def desynthesize(s: String): Option[String] = + val i = s.indexOf('$') + if 0 until s.length - 1 contains i then + val name = s.substring(0, i).nn + val sufx = s.substring(i) + + def loadableOrNone(strip: Boolean) = + def suffix(strip: Boolean)(x: String) = + (if strip && x.endsWith("$") then x.init else x) + sufx + repl.driver.binaryClassOfType(name) + .map(suffix(strip)(_)) + .filter(loadable) + + // try loading translated+suffix + // some synthetics lack a dollar, (e.g., suffix = delayedInit$body) + // so as a hack, if prefix$$suffix fails, also try prefix$suffix + loadableOrNone(strip = false) + .orElse(loadableOrNone(strip = true)) + else + None + end desynthesize + + def scopedClass(name: String): Option[String] = repl.driver.binaryClassOfType(name).filter(loadable) + def enclosingClass(name: String): Option[String] = repl.driver.binaryClassOfTerm(name).filter(loadable) + def qualifiedName(name: String): Option[String] = Some(name).filter(_.contains('.')).filter(loadable) + + val p = path.asClassName // scrub any suffix + val className = + qualifiedName(p) + .orElse(scopedClass(p)) + .orElse(enclosingClass(p)) + .orElse(desynthesize(p)) + .getOrElse(p) + + val classBytes = loader.classBytes(className) + if classBytes.isEmpty then + Failure(FileNotFoundException(s"Could not find class bytes for '$path'")) + else + Success(className, classBytes) + end tryClass + +object DisassemblyClass: + private final val classSuffix = ".class" + + /** Match foo#bar, both groups are optional (may be null). */ + @sharable private val HashSplit = "([^#]+)?(?:#(.+)?)?".r + + // We enjoy flexibility in specifying either a fully-qualified class name com.acme.Widget + // or a resource path com/acme/Widget.class; but not widget.out + extension (s: String) + def asClassName = s.stripSuffix(classSuffix).replace('/', '.').nn + def asClassResource = if s.endsWith(classSuffix) then s else s.replace('.', '/').nn + classSuffix + + extension (cl: ClassLoader) + /** Would classBytes succeed with a nonempty array */ + def resourceable(className: String): Boolean = + cl.getResource(className.asClassResource) != null +end DisassemblyClass diff --git a/compiler/src/dotty/tools/repl/ReplDriver.scala b/compiler/src/dotty/tools/repl/ReplDriver.scala index 1093304496fa..fe1bba9317f5 100644 --- a/compiler/src/dotty/tools/repl/ReplDriver.scala +++ b/compiler/src/dotty/tools/repl/ReplDriver.scala @@ -7,6 +7,7 @@ import java.nio.charset.StandardCharsets import dotty.tools.dotc.ast.Trees._ import dotty.tools.dotc.ast.{tpd, untpd} +import dotty.tools.dotc.ast.tpd.TreeOps import dotty.tools.dotc.config.CommandLineParser.tokenize import dotty.tools.dotc.config.Properties.{javaVersion, javaVmName, simpleVersionString} import dotty.tools.dotc.core.Contexts._ @@ -20,7 +21,7 @@ import dotty.tools.dotc.core.NameKinds.DefaultGetterName import dotty.tools.dotc.core.NameOps._ import dotty.tools.dotc.core.Names.Name import dotty.tools.dotc.core.StdNames._ -import dotty.tools.dotc.core.Symbols.{Symbol, defn} +import dotty.tools.dotc.core.Symbols._ import dotty.tools.dotc.interfaces import dotty.tools.dotc.interactive.Completion import dotty.tools.dotc.printing.SyntaxHighlighting @@ -115,6 +116,8 @@ class ReplDriver(settings: Array[String], rendering = new Rendering(classLoader) } + private[repl] def replClassLoader()(using Context) = rendering.classLoader() + private var rootCtx: Context = _ private var shouldStart: Boolean = _ private var compiler: ReplCompiler = _ @@ -325,7 +328,27 @@ class ReplDriver(settings: Array[String], ) } - private def renderDefinitions(tree: tpd.Tree, newestWrapper: Name)(implicit state: State): (State, Seq[Diagnostic]) = { + private def collectTypeDefs[A](sym: Symbol)(f: Denotation => A)(using Context): Seq[A] = + sym.info.memberClasses.collect { + case x if !x.symbol.isSyntheticCompanion && !x.symbol.name.isReplWrapperName => + f(x) + } + + private def extractDefMembers(sym: Symbol)(using Context): Seq[Denotation] = + sym.info.bounds.hi.finalResultType + .membersBasedOnFlags(required = Method, excluded = Accessor | ParamAccessor | Synthetic | Private) + .filterNot(denot => + defn.topClasses.contains(denot.symbol.owner) + || denot.symbol.isConstructor + || denot.symbol.name.is(DefaultGetterName) + ) + + private def extractValMembers(sym: Symbol)(using Context): Seq[Denotation] = + sym.info.fields + .filterNot(_.symbol.isOneOf(ParamAccessor | Private | Synthetic | Artifact | Module)) + .filter(_.symbol.name.is(SimpleNameKind)) + + private def renderDefinitions(tree: tpd.Tree, newestWrapper: Name)(implicit state: State): (State, Seq[Diagnostic]) = given Context = state.context def resAndUnit(denot: Denotation) = { @@ -339,62 +362,40 @@ class ReplDriver(settings: Array[String], name.startsWith(str.REPL_RES_PREFIX) && hasValidNumber && sym.info == defn.UnitType } - def extractAndFormatMembers(symbol: Symbol): (State, Seq[Diagnostic]) = if (tree.symbol.info.exists) { - val info = symbol.info - val defs = - info.bounds.hi.finalResultType - .membersBasedOnFlags(required = Method, excluded = Accessor | ParamAccessor | Synthetic | Private) - .filterNot { denot => - defn.topClasses.contains(denot.symbol.owner) || denot.symbol.isConstructor - || denot.symbol.name.is(DefaultGetterName) - } - - val vals = - info.fields - .filterNot(_.symbol.isOneOf(ParamAccessor | Private | Synthetic | Artifact | Module)) - .filter(_.symbol.name.is(SimpleNameKind)) - - val typeAliases = - info.bounds.hi.typeMembers.filter(_.symbol.info.isTypeAlias) - - // The wrapper object may fail to initialize if the rhs of a ValDef throws. - // In that case, don't attempt to render any subsequent vals, and mark this - // wrapper object index as invalid. - var failedInit = false - val renderedVals = - val buf = mutable.ListBuffer[Diagnostic]() - for d <- vals do if !failedInit then rendering.renderVal(d) match - case Right(Some(v)) => - buf += v - case Left(e) => - buf += rendering.renderError(e, d) - failedInit = true - case _ => - buf.toList - - if failedInit then - // We limit the returned diagnostics here to `renderedVals`, which will contain the rendered error - // for the val which failed to initialize. Since any other defs, aliases, imports, etc. from this - // input line will be inaccessible, we avoid rendering those so as not to confuse the user. - (state.copy(invalidObjectIndexes = state.invalidObjectIndexes + state.objectIndex), renderedVals) - else - val formattedMembers = - typeAliases.map(rendering.renderTypeAlias) - ++ defs.map(rendering.renderMethod) - ++ renderedVals - val diagnostics = if formattedMembers.isEmpty then rendering.forceModule(symbol) else formattedMembers - (state.copy(valIndex = state.valIndex - vals.count(resAndUnit)), diagnostics) - } - else (state, Seq.empty) - - def isSyntheticCompanion(sym: Symbol) = - sym.is(Module) && sym.is(Synthetic) - - def typeDefs(sym: Symbol): Seq[Diagnostic] = sym.info.memberClasses - .collect { - case x if !isSyntheticCompanion(x.symbol) && !x.symbol.name.isReplWrapperName => - rendering.renderTypeDef(x) - } + def extractAndFormatMembers(symbol: Symbol): (State, Seq[Diagnostic]) = + if tree.symbol.info.exists then + val defs = extractDefMembers(symbol) + val vals = extractValMembers(symbol) + val typeAliases = symbol.info.bounds.hi.typeMembers.filter(_.symbol.info.isTypeAlias) + + // The wrapper object may fail to initialize if the rhs of a ValDef throws. + // In that case, don't attempt to render any subsequent vals, and mark this + // wrapper object index as invalid. + var failedInit = false + val renderedVals = + val buf = mutable.ListBuffer[Diagnostic]() + for d <- vals do if !failedInit then rendering.renderVal(d) match + case Right(Some(v)) => + buf += v + case Left(e) => + buf += rendering.renderError(e, d) + failedInit = true + case _ => + buf.toList + + if failedInit then + // We limit the returned diagnostics here to `renderedVals`, which will contain the rendered error + // for the val which failed to initialize. Since any other defs, aliases, imports, etc. from this + // input line will be inaccessible, we avoid rendering those so as not to confuse the user. + (state.copy(invalidObjectIndexes = state.invalidObjectIndexes + state.objectIndex), renderedVals) + else + val formattedMembers = + typeAliases.map(rendering.renderTypeAlias) + ++ defs.map(rendering.renderMethod) + ++ renderedVals + val diagnostics = if formattedMembers.isEmpty then rendering.forceModule(symbol) else formattedMembers + (state.copy(valIndex = state.valIndex - vals.count(resAndUnit)), diagnostics) + else (state, Seq.empty) atPhase(typerPhase.next) { // Display members of wrapped module: @@ -404,7 +405,7 @@ class ReplDriver(settings: Array[String], val (newState, formattedMembers) = extractAndFormatMembers(wrapperModule.symbol) val formattedTypeDefs = // don't render type defs if wrapper initialization failed if newState.invalidObjectIndexes.contains(state.objectIndex) then Seq.empty - else typeDefs(wrapperModule.symbol) + else collectTypeDefs(wrapperModule.symbol)(rendering.renderTypeDef) val highlighted = (formattedTypeDefs ++ formattedMembers) .map(d => new Diagnostic(d.msg.mapMsg(SyntaxHighlighting.highlight), d.pos, d.level)) (newState, highlighted) @@ -414,7 +415,7 @@ class ReplDriver(settings: Array[String], (state, Seq.empty) } } - } + end renderDefinitions /** Interpret `cmd` to action and propagate potentially new `state` */ private def interpretCommand(cmd: Command)(implicit state: State): State = cmd match { @@ -498,6 +499,19 @@ class ReplDriver(settings: Array[String], state } + private def disassemble(tool: Disassembler, opts: DisassemblerOptions)(using DisassemblerRepl): Unit = + if opts.targets.isEmpty || opts.flags.contains("-help") then + out.println(tool.helpText) + else + import DisResult._ + tool(opts).foreach { + case DisSuccess(target, results) => + val filter = tool.outputFilter(target, opts) + out.println(filter(results)) + case DisError(err) => + out.println(err) + } + /** shows all errors nicely formatted */ private def displayErrors(errs: Seq[Diagnostic])(implicit state: State): State = { errs.foreach(printDiagnostic) @@ -517,4 +531,90 @@ class ReplDriver(settings: Array[String], case interfaces.Diagnostic.INFO => out.println(dia.msg) // print REPL's special info diagnostics directly to out case _ => ReplConsoleReporter.doReport(dia)(using state.context) + extension (sym: Symbol)(using Context) + // borrowed from ExtractDependencies#recordDependency and adapted to handle @targetName + private def binaryClassName: String = + val builder = new StringBuilder + val pkg = sym.enclosingPackageClass + if !pkg.isEffectiveRoot then + builder.append(pkg.fullName.mangledString) + builder.append(".") + import dotty.tools.dotc.core.NameKinds._ + val flatName = sym.maybeOwner.fullNameSeparated(FlatName, FlatName, sym.targetName) + val clsFlatName = if sym.is(JavaDefined) then flatName.stripModuleClassSuffix else flatName + builder.append(clsFlatName.mangledString) + builder.toString + + private def isSyntheticCompanion = + sym.is(Module) && sym.is(Synthetic) + end extension + + /** Returns a list of disassembly targets for the most recent REPL input. + * This computes the targets when "-" is given to the :javap or :asmp command. + * The targets include all top level classes defined by the REPL input, + * plus the REPL wrapper object itself if any vals or defs were defined. + */ + def disassemblyTargetsLastWrapper(state0: State): List[String] = + given state: State = newRun(state0) + given Context = state.context + + def hasMembers(sym: Symbol): Boolean = + extractDefMembers(sym).nonEmpty || extractValMembers(sym).nonEmpty + + val lastWrapper = s"${str.REPL_SESSION_LINE}${state0.objectIndex}" + val wrapperModuleClass = List(lastWrapper + "$") + + compiler.typeCheck(lastWrapper).map(tree => + tree.rhs match + case Block(id :: Nil, _) => + val sym = id.tpe.typeSymbol + collectTypeDefs(sym)(_.symbol.binaryClassName).toList + ++ (if hasMembers(sym) then wrapperModuleClass else Nil) + ) + .toOption + .filterNot(_.isEmpty) + .getOrElse(wrapperModuleClass) + end disassemblyTargetsLastWrapper + + /** Is `name` a type symbol in REPL scope? + * Returns Some containing its binary class name if so, otherwise None + */ + def binaryClassOfType(name: String)(using state0: State): Option[String] = + given state: State = newRun(state0) + given Context = state.context + + val typeName = + if name.endsWith("$") then s"${name.init}.type" + else name + + compiler.typeCheck(s"type $$_ = $typeName", errorsAllowed = true).toOption.flatMap(tree => + tree.rhs match + case Block(List(TypeDef(_, x)), _) => + val sym = x.tpe.widenDealias.typeSymbol + Option.when(sym.exists && sym.isClass)(sym.binaryClassName) + ) + end binaryClassOfType + + /** Is `name` a symbol in some enclosing class scope? + * Returns Some containing its binary class name if so, otherwise None + */ + def binaryClassOfTerm(name: String)(using state0: State): Option[String] = + given state: State = newRun(state0) + given Context = state.context + + def extractSymbol(tree: tpd.Tree): Symbol = tree match + case tpd.closureDef(defdef) => defdef.rhs.symbol + case _ => tree.symbol + + compiler.typeCheck(s"val $$_ = $name").toOption.flatMap(tree => + tree.rhs match + case Block((valdef: tpd.ValDef) :: Nil, _) => + val sym = extractSymbol(valdef.rhs) + val encl = + if sym.is(ModuleVal) then sym.moduleClass + else sym.lexicallyEnclosingClass + Option.when(encl.exists)(encl.binaryClassName) + ) + end binaryClassOfTerm + end ReplDriver diff --git a/compiler/src/dotty/tools/repl/ReplStrings.scala b/compiler/src/dotty/tools/repl/ReplStrings.scala new file mode 100644 index 000000000000..6b71d6a3180b --- /dev/null +++ b/compiler/src/dotty/tools/repl/ReplStrings.scala @@ -0,0 +1,12 @@ +package dotty.tools.repl + +import scala.language.unsafeNulls + +import scala.annotation.internal.sharable + +object ReplStrings { + // no escaped or nested quotes + @sharable private val inquotes = """(['"])(.*?)\1""".r + def unquoted(s: String) = s match { case inquotes(_, w) => w ; case _ => s } + def words(s: String) = (s.trim split "\\s+" filterNot (_ == "") map (unquoted _)).toList +} diff --git a/compiler/src/dotty/tools/runner/ScalaClassLoader.scala b/compiler/src/dotty/tools/runner/ScalaClassLoader.scala index 03819f36d69f..30ba62701191 100644 --- a/compiler/src/dotty/tools/runner/ScalaClassLoader.scala +++ b/compiler/src/dotty/tools/runner/ScalaClassLoader.scala @@ -34,6 +34,16 @@ object ClassLoaderOps: catching(classOf[ClassNotFoundException], classOf[SecurityException]) opt Class.forName(path, initialize, self).asInstanceOf[Class[T]] + /** The actual bytes for a class file, or an empty array if it can't be found. */ + def classBytes(className: String): Array[Byte] = classAsStream(className) match + case null => Array() + case stream => dotty.tools.io.Streamable.bytes(stream) + + private inline def classAsStream(className: String) = self.getResourceAsStream { + if className.endsWith(".class") then className + else s"${className.replace('.', '/')}.class" // classNameToPath + } + /** Run the main method of a class to be loaded by this classloader */ def runMain(objectName: String, arguments: Seq[String]): Unit = val clsToRun = tryToInitializeClass(objectName).getOrElse(throw ClassNotFoundException(objectName)) diff --git a/compiler/test/dotty/tools/repl/DisassemblerTests.scala b/compiler/test/dotty/tools/repl/DisassemblerTests.scala new file mode 100644 index 000000000000..307fecd914c4 --- /dev/null +++ b/compiler/test/dotty/tools/repl/DisassemblerTests.scala @@ -0,0 +1,250 @@ +package dotty.tools.repl + +import org.junit.Test +import org.junit.Assert.assertEquals +import scala.annotation.tailrec +import dotty.tools.dotc.core.StdNames.{nme, str} + +abstract class ReplDisassemblerTest extends ReplTest: + def packageSeparator: String = "." + def eval(code: String): State = + val state = initially { run(code) } + val _ = storedOutput() // discard output + state + def line(n: Int): String = s"${str.REPL_SESSION_LINE}$n" + def line(n: Int, rest: String): String = line(n) + "$" + rest + +// Test option parsing +class DisassemblerOptionParsingTests extends ReplDisassemblerTest: + private val helps = List( + "usage" -> ":tool [opts] [path or class or -]...", + "-verbose/-v" -> "Stack size, number of locals, method args", + "-private/-p" -> "Private classes and members", + "-protected" -> "Protected classes and members", + "-s" -> "Internal type signatures", + "-sysinfo" -> "System info of class", + "-filter" -> "Filter REPL machinery from output", + ) + private val options = new DisassemblerOptionParser(helps): + val defaultToolOptions = List("-protected", "-verbose") + + private def parsed(input: Seq[String])(using state: State): DisassemblerOptions = + given DisassemblerRepl(this, state) + options.parse(input) + + private def assertTargets(expected: Seq[String], input: Seq[String])(using State) = + assertEquals(expected, parsed(input).targets) + + private def assertFlags(expected: Seq[String], input: Seq[String])(using State) = + assertEquals(expected, parsed(input).flags) + + private def assertFilter(expected: Boolean, input: Seq[String])(using State) = + assertEquals(expected, parsed(input).filterReplNames) + + // disassembly targets are extracted properly + @Test def targets = + eval("class XYZ").andThen { + assertTargets(Nil, Nil) + assertTargets(Seq("t1", "t2#m"), Seq("t1", "t2#m")) + assertTargets(Seq(line(1, "XYZ"), "/tmp/t2.class"), Seq("-pro", "-filter", "-verb", "-", "/tmp/t2.class")) + assertTargets(Seq("t1.class", "scala.util.Either#orElse"), Seq("-s", "t1.class", "-filter", "-pri", "scala.util.Either#orElse")) + } + + // normal flags are set properly + @Test def flags = + initially { + assertFlags(options.defaultToolOptions, Nil) + assertFlags(Seq("-p"), Seq("-p", "target")) + assertFlags(Seq("-private"), Seq("-private", "target")) + assertFlags(Seq("-sysinfo", "-v"), Seq("-sys", "-v", "target")) + assertFlags(Seq("-protected", "-verbose"), Seq("-pro", "-filter", "-verb", "t1", "t2")) + assertFlags(Seq("-s", "-private"), Seq("-s", "t1", "-filter", "-pri", "t2")) + } + + // unrecognized flags should result in -help + @Test def flagsUnknown = + initially { + assertFlags(Seq("-help"), Seq("-unknown", "target")) + } + + // the special `-filter` flag is extracted and set properly + @Test def optionFilter = + initially { + assertFilter(false, Nil) + assertFilter(false, Seq("-p", "-v", "target")) + assertFilter(false, Seq("-", "filter", "target")) + assertFilter(true, Seq("-filter", "target")) + assertFilter(true, Seq("-filt", "target")) + assertFilter(true, Seq("-p", "-f", "-v", "target")) + } +end DisassemblerOptionParsingTests + +// Test target resolution +// TODO expand on what that means +class DisassemblyTargetTests extends ReplDisassemblerTest: + import DisassemblyTool.Input + + private def assertTargets(expected: Seq[String], targets: Seq[String])(using state: State): Unit = + given repl: DisassemblerRepl = DisassemblerRepl(this, state) + val clazz = DisassemblyClass(repl.classLoader) + val targetNames = targets.flatMap(t => if t == "-" then repl.mostRecentEntry else Seq(t)) + assertEquals(expected, targetNames.map(clazz.bytes(_).actual)) + + private def assertTarget(expected: String, target: String)(using State): Unit = + assertTargets(expected :: Nil, target :: Nil) + + @Test def targetOfStdlib = + initially { + assertTarget("scala.collection.immutable.List", "List") + assertTarget("scala.collection.immutable.List$", "List$") + assertTarget("scala.collection.immutable.List$", "List.type") + assertTarget("scala.collection.immutable.List", "List#tail") + } + + @Test def targetOfJavaStdlib = + initially { + assertTarget("java.lang.String", "String#substring") + assertTarget("java.lang.Character$UnicodeBlock", "Character.UnicodeBlock") + } + + @Test def targetOfEmptyClass = + eval("class C").andThen { + assertTarget(line(1, "C"), "C") + } + + @Test def targetOfEnum = + eval( + """enum Color { + | case Red, Green, Blue + |} + """.stripMargin + ).andThen { + assertTarget(line(1, "Color"), "Color") + assertTarget(line(1, "Color$"), "Color$") + assertTarget(line(1, "Color$"), "Color.type") + assertTarget(line(1, "Color$"), "Color.Green") + } + + @Test def targetOfClassInsideClass = + eval( + """class Greeting { + | class D { + | def howdy() = println("howdy") + | } + |} + """.stripMargin + ).andThen { + assertTarget(line(1, "Greeting"), "Greeting") + assertTarget(line(1, "Greeting$D"), "Greeting$D") // resolved by desynthesize() + //assertTarget(line(1, "Greeting$D"), "Greeting#D") // XXX fails, triggers filtering instead of inner class selection + } + + @Test def targetOfClassInsideObject = + eval( + """object Hello { + | class C { + | def hello() = println("hi") + | } + |} + """.stripMargin + ).andThen { + assertTarget(line(1, "Hello$"), "Hello") + assertTarget(line(1, "Hello$C"), "Hello.C") + //assertTarget(line(1, "Hello$C"), "Hello$C") // XXX fails to resolve, desynthesize expects prefix to be a type + } + + @Test def targetOfTypeConstructor = + eval( + """class Id[A] { + | def me(in: A): A = in + |} + """.stripMargin + ).andThen { + assertTarget(line(1, "Id"), "Id") + assertTarget(line(1, "Id"), "Id[?]") + assertTarget(line(1, "Id"), "Id#me") + } + + @Test def targetOfTypeAlias = + eval( + """class IntAdder { + | def addOne(x: Int) = x + 1 + |} + |type IA = IntAdder + """.stripMargin + ).andThen { + assertTarget(line(1, "IntAdder"), "IA") + assertTarget(line(1, "IntAdder"), "IA#addOne") + } + + @Test def targetOfTargetNameClass = + eval( + """import scala.annotation.targetName + |@targetName("Target") class Defined { + | def self: Defined = this + |} + """.stripMargin + ).andThen { + assertTarget(line(1, "Target"), "Defined") + assertTarget("Target", "Target") // fall back to verbatim requested target + } + + @Test def targetOfSimpleVal = + eval("val x = 42").andThen { + assertTarget(line(1, ""), "x") + } + + @Test def targetOfNullaryMethod = + eval("def nonRandom() = 42").andThen { + assertTarget(line(1, ""), "nonRandom()") + } + + @Test def targetOfJavaStaticVal = + initially { + assertTarget("java.time.DayOfWeek", "java.time.DayOfWeek.MONDAY") + } + + @Test def targetOfLast = + eval("class C").andThen { assertTarget(line(1, "C"), "-") } + eval("object X").andThen { assertTarget(line(1, "X$"), "-") } + eval("val t = 0").andThen { assertTarget(line(1, ""), "-") } + eval("def f = 10").andThen { assertTarget(line(1, ""), "-") } + eval( + """class C + |class D + """.stripMargin + ).andThen { + assertTargets(List(line(1, "C"), line(1, "D")), Seq("-")) + } + eval( + """import scala.annotation.targetName + |@targetName("Target") class Defined + """.stripMargin + ).andThen { + assertTarget(line(1, "Target"), "-") + } +end DisassemblyTargetTests + +abstract class DisassemblerTest extends ReplDisassemblerTest: + def assertDisassemblyIncludes(line: String, output: String | Null = null): Unit = + val out = if output == null then storedOutput() else output + assert(out.linesIterator.exists(_.contains(line)), + s"disassembly did not contain `$line`\nDisassembly was:\n$out") + + // NB: supplied expected lines must occur in the same order in the output + def assertDisassemblyIncludes(lines: List[String]): Unit = + val out = storedOutput() + @tailrec def loop(input: Iterator[String], expected: List[String]): Unit = + expected match + case Nil => + case x :: xs => + val it = input.dropWhile(!_.contains(x)) + assert(it.hasNext, s"disassembly did not contain `$x`\nDisassembly was:\n$out") + loop(it.drop(1), xs) + loop(out.linesIterator, lines) + + def assertDisassemblyExcludes(line: String, output: String | Null = null): Unit = + val out = if output == null then storedOutput() else output + assert(!out.linesIterator.exists(_.contains(line)), + s"disassembly unexpectedly contained `$line`") +end DisassemblerTest From b39e3e8c42e197764d788f8945966a1f618dd515 Mon Sep 17 00:00:00 2001 From: Tom Grigg Date: Thu, 17 Mar 2022 19:46:07 -0700 Subject: [PATCH 3/6] Add `:javap` to the REPL Provides bytecode disassembly using the `javap` tool/interface supplied by the user's JDK. Adapted from the Scala 2 implementation, which was written by Paul Phillips and Som Snytt / A. P. Marki --- .../tools/dotc/config/PathResolver.scala | 41 ++ .../dotty/tools/dotc/config/Properties.scala | 1 + .../src/dotty/tools/repl/Disassembler.scala | 356 ++++++++++++++++++ .../src/dotty/tools/repl/ParseResult.scala | 8 + .../src/dotty/tools/repl/ReplDriver.scala | 6 + .../dotty/tools/runner/ScalaClassLoader.scala | 27 ++ .../dotty/tools/repl/DisassemblerTests.scala | 47 +++ .../dotty/tools/repl/TabcompleteTests.scala | 1 + 8 files changed, 487 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/config/PathResolver.scala b/compiler/src/dotty/tools/dotc/config/PathResolver.scala index afa30e38dc2a..31a09d9fd927 100644 --- a/compiler/src/dotty/tools/dotc/config/PathResolver.scala +++ b/compiler/src/dotty/tools/dotc/config/PathResolver.scala @@ -129,6 +129,47 @@ object PathResolver { ) } + /** Locations discovered by supplemental heuristics. + */ + object SupplementalLocations { + + /** The platform-specific support jar. + * + * Usually this is `tools.jar` in the jdk/lib directory of the platform distribution. + * + * The file location is determined by probing the lib directory under JDK_HOME or JAVA_HOME, + * if one of those environment variables is set, then the lib directory under java.home, + * and finally the lib directory under the parent of java.home. Or, as a last resort, + * search deeply under those locations (except for the parent of java.home, on the notion + * that if this is not a canonical installation, then that search would have little + * chance of succeeding). + */ + def platformTools: Option[File] = { + val jarName = "tools.jar" + def jarPath(path: Path) = (path / "lib" / jarName).toFile + def jarAt(path: Path) = { + val f = jarPath(path) + if (f.isFile) Some(f) else None + } + val jdkDir = { + val d = Directory(jdkHome) + if (d.isDirectory) Some(d) else None + } + def deeply(dir: Directory) = dir.deepFiles find (_.name == jarName) + + val home = envOrSome("JDK_HOME", envOrNone("JAVA_HOME")) map (p => Path(p)) + val install = Some(Path(javaHome)) + + (home flatMap jarAt) orElse (install flatMap jarAt) orElse (install map (_.parent) flatMap jarAt) orElse + (jdkDir flatMap deeply) + } + + override def toString = s""" + |object SupplementalLocations { + | platformTools = $platformTools + |}""".trim.stripMargin + } + def fromPathString(path: String)(using Context): ClassPath = { val settings = ctx.settings.classpath.update(path) inContext(ctx.fresh.setSettings(settings)) { diff --git a/compiler/src/dotty/tools/dotc/config/Properties.scala b/compiler/src/dotty/tools/dotc/config/Properties.scala index 1e9cc82112af..aee943065f60 100644 --- a/compiler/src/dotty/tools/dotc/config/Properties.scala +++ b/compiler/src/dotty/tools/dotc/config/Properties.scala @@ -55,6 +55,7 @@ trait PropertiesTrait { def envOrElse(name: String, alt: String): String = Option(System getenv name) getOrElse alt def envOrNone(name: String): Option[String] = Option(System getenv name) + def envOrSome(name: String, alt: => Option[String]) = envOrNone(name) orElse alt // for values based on propFilename def scalaPropOrElse(name: String, alt: String): String = scalaProps.getProperty(name, alt) diff --git a/compiler/src/dotty/tools/repl/Disassembler.scala b/compiler/src/dotty/tools/repl/Disassembler.scala index 7298210ed170..eec2fb471a5e 100644 --- a/compiler/src/dotty/tools/repl/Disassembler.scala +++ b/compiler/src/dotty/tools/repl/Disassembler.scala @@ -271,3 +271,359 @@ object DisassemblyClass: def resourceable(className: String): Boolean = cl.getResource(className.asClassResource) != null end DisassemblyClass + +/** A disassembler implemented using the `javap` tool from the JDK + * TODO: note the various mechanisms/providers that may be used. + */ +object Javap extends Disassembler: + import Disassembler.* + import dotc.core.Decorators.* + import dotc.core.NameOps.* + + // load and run a tool + def apply(opts: DisassemblerOptions)(using DisassemblerRepl): List[DisResult] = + selectProvider + .map(_.runOn(opts)) + .getOrElse(DisError("Unable to locate a usable javap provider.") :: Nil) + + def selectProvider(using repl: DisassemblerRepl): Option[JavapProvider] = + def maybeTask = JavapTaskProvider(repl).probed() + def maybeTool = JavapToolProvider(repl).probed() + maybeTask orElse maybeTool + + // Filtering output. Additional filters provided by this disassembler: + // - limit the disassembly output to selected method in the target, e.g. Klass#method + + override def filters(target: String, opts: DisassemblerOptions): List[String => String] = + val commonFilters = super.filters(target, opts) + if target.indexOf('#') != -1 then filterSelection(target) :: commonFilters + else commonFilters + + // filter lines of javap output for target such as Klass#method + def filterSelection(target: String)(text: String): String = + // take Foo# as Foo#apply for purposes of filtering. + val filterOn = splitHashMember(target).map(s => if s.isEmpty then "apply" else s) + var filtering = false // true if in region matching filter + + // turn filtering on/off given the pattern of interest + def filterStatus(line: String, pattern: String) = + def isSpecialized(method: String) = method.startsWith(pattern + "$") && method.endsWith("$sp") + def isAnonymized(method: String) = (pattern == str.ANON_FUN) && method.toTermName.isAnonymousFunctionName + + // cheap heuristic, todo maybe parse for the java sig. + // method sigs end in paren semi + def isAnyMethod = line.endsWith(");") + + // take the method name between the space char and left paren. + // accept exact match or something that looks like what we might be asking for. + def isOurMethod = + val lparen = line.lastIndexOf('(') + val blank = line.lastIndexOf(' ', lparen) + if blank < 0 then false + else + val method = line.substring(blank + 1, lparen).nn + (method == pattern || isSpecialized(method) || isAnonymized(method)) + + // next blank line or line containing only "}" terminates section + def isSectionEnd = line == "}" || line.trim.nn.isEmpty + + filtering = + if filtering then + // in non-verbose mode, next line is next method, more or less + !isSectionEnd && (!isAnyMethod || isOurMethod) + else + isAnyMethod && isOurMethod + + filtering + end filterStatus + + // do we output this line? + def checkFilter(line: String) = filterOn.map(filterStatus(line, _)).getOrElse(true) + filteredLines(text, checkFilter) + end filterSelection + + // Help/options + val helps = List( + "usage" -> ":javap [opts] [path or class or -]...", + "-help" -> "Prints this help message", + "-verbose/-v" -> "Stack size, number of locals, method args", + "-private/-p" -> "Private classes and members", + "-package" -> "Package-private classes and members", + "-protected" -> "Protected classes and members", + "-public" -> "Public classes and members", + "-l" -> "Line and local variable tables", + "-c" -> "Disassembled code", + "-s" -> "Internal type signatures", + "-sysinfo" -> "System info of class", + "-constants" -> "Static final constants", + "-filter" -> "Filter REPL machinery from output", + ) +end Javap + +object JavapOptions extends DisassemblerOptionParser(Javap.helps): + val defaultToolOptions = List("-protected", "-verbose") + +/** A provider of a javap-based DisassemblyTool implementation. + * The provider selected is influenced by the JDK in use, and probed for availability. + */ +abstract class JavapProvider(protected val repl: DisassemblerRepl): + /** The classloader from which the tool is loaded. */ + def loader: Either[String, ClassLoader] + + /** Find the javap tool, which may be unavailable or fail to load. */ + def findTool(loader: ClassLoader): Either[String, DisassemblyTool] + + /** Returns Some(this) if this provider is available at runtime, otherwise None. + * Note that this may attempt to reflectively load and initialize classes from + * `loader` to confirm the availability of the provider. + */ + def probed(): Option[this.type] + + /** Run the tool on the class bytes of each target and collect results. */ + final def runOn(opts: DisassemblerOptions): List[DisResult] = + val results = for + cl <- loader + tool <- findTool(cl) + clazz = DisassemblyClass(cl)(using repl) + yield tool(opts.flags)(opts.targets.map(clazz.bytes(_))) + results.fold(msg => List(DisError(msg)), identity) +end JavapProvider + +/** JDK9+ to locate ToolProvider. */ +class JavapToolProvider(repl0: DisassemblerRepl) extends JavapProvider(repl0): + import java.io.{ByteArrayOutputStream, PrintStream} + import java.nio.file.Files + import java.util.Optional + import scala.reflect.Selectable.reflectiveSelectable + import scala.util.Properties.isJavaAtLeast + import scala.util.Using + import dotty.tools.io.File + import DisassemblyTool.Input + + type ToolProvider = { def run(out: PrintStream, err: PrintStream, args: Array[String]): Unit } + + override def loader: Either[String, ClassLoader] = Right(repl.classLoader) + + override def probed(): Option[this.type] = Option.when(isJavaAtLeast("9"))(this) + + private def createTool(provider: ToolProvider) = new DisassemblyTool: + override def apply(options: Seq[String])(inputs: Seq[Input]): List[DisResult] = + def runInput(input: Input): DisResult = input match + case Input(target, _, Success(bytes)) => runFromTempFile(target, bytes) + case Input(_, _, Failure(e)) => DisError(e.toString) + + def runFromTempFile(target: String, bytes: Array[Byte]): DisResult = + Try(File(Files.createTempFile("scala3-repl-javap", ".class").nn)) match + case Success(tmp) => + val res = Using(tmp.bufferedOutput())(_.write(bytes)) match + case Success(_) => runTool(target, options :+ tmp.path) + case Failure(e) => DisError(e.toString) + tmp.delete() + res + case Failure(e) => DisError(e.toString) + + def runTool(target: String, args: Seq[String]): DisResult = + val out = ByteArrayOutputStream() + val outPrinter = PrintStream(out) + val rc = + reflectiveSelectable(provider) // work around lampepfl/dotty#11043 + .applyDynamic("run", classOf[PrintStream], classOf[PrintStream], classOf[Array[String]]) + (outPrinter, outPrinter, args.toArray) + val output = out.toString + if rc == 0 then DisSuccess(target, output) else DisError(output) + + inputs.map(runInput).toList + end apply + end createTool + + //ToolProvider.findFirst("javap") + override def findTool(loader: ClassLoader): Either[String, DisassemblyTool] = + val provider = Class.forName("java.util.spi.ToolProvider", /*initialize=*/ true, loader).nn + .getDeclaredMethod("findFirst", classOf[String]).nn + .invoke(null, "javap") + .asInstanceOf[Optional[ToolProvider]] + if provider.isPresent then Right(createTool(provider.get.nn)) + else Left(s":javap unavailable: provider not found") +end JavapToolProvider + +/** A JavapProvider using the JDK internal API JavapTask as its DisassemblyTool. + * This may require adding the JDK tools.jar to the classpath. + * The JavapTask API may not be available (especially on newer JDKs), + * which will disqualify this provider. + */ +class JavapTaskProvider(repl0: DisassemblerRepl) extends JavapProvider(repl0): + import scala.util.Properties.{isJavaAtLeast, jdkHome} + import dotty.tools.dotc.config.PathResolver + import dotty.tools.io.File + import dotty.tools.runner.ClassLoaderOps.* + import java.net.URL + + private val isJava8 = !isJavaAtLeast("9") + + private def findToolsJar(): Option[File] = + if isJava8 then PathResolver.SupplementalLocations.platformTools + else None + + private def addToolsJarToLoader(): ClassLoader = + findToolsJar() match + case Some(tools) => java.net.URLClassLoader(Array[URL | Null](tools.toURL), repl.classLoader) + case _ => repl.classLoader + + override def loader: Either[String, ClassLoader] = + def noTools = if isJava8 then s" or no tools.jar at $jdkHome" else "" + Right(addToolsJarToLoader()).filterOrElse( + _.tryToInitializeClass[AnyRef](JavapTask.taskClassName).isDefined, + s":javap unavailable: no ${JavapTask.taskClassName}$noTools" + ) + + override def findTool(loader: ClassLoader): Either[String, DisassemblyTool] = + Try(JavapTask(loader)) match + case Success(tool) => Right(tool) + case Failure(e) => Left(e.toString) + + override def probed(): Option[this.type] = + if isJava8 then Some(this) + else Option.when(loader.flatMap(cl => findTool(cl)).isRight)(this) +end JavapTaskProvider + +/** A DisassemblyTool implementation using the JDK internal API `JavapTask` + * introduced by JDK7. This API supports consuming an array of bytes as + * its input. The task is run using reflection. + * + * This approach may be unusable in JDK+ (and especially JDK 16+) due to the + * Java module system and ever-increasing restrictions. + */ +class JavapTask(val loader: ClassLoader) extends DisassemblyTool: + import javax.tools.{Diagnostic, DiagnosticListener, + ForwardingJavaFileManager, JavaFileManager, JavaFileObject, + SimpleJavaFileObject, StandardLocation} + import java.io.{CharArrayWriter, PrintWriter} + import java.util.Locale + import java.util.concurrent.ConcurrentLinkedQueue + import scala.jdk.CollectionConverters.* + import scala.collection.mutable.Clearable + import scala.reflect.Selectable.reflectiveSelectable + import dotty.tools.runner.ClassLoaderOps.* + import DisassemblyTool.* + + // output filtering support + val writer = new CharArrayWriter + def written() = + writer.flush() + val w = writer.toString + writer.reset() + w + + type Task = { def call(): Boolean } + + class JavaReporter extends DiagnosticListener[JavaFileObject] with Clearable: + type D = Diagnostic[? <: JavaFileObject] + val diagnostics = new ConcurrentLinkedQueue[D] + override def report(d: Diagnostic[? <: JavaFileObject]) = diagnostics.add(d) + override def clear() = diagnostics.clear() + /** All diagnostic messages. + * @param locale Locale for diagnostic messages, null by default. + */ + def messages(using locale: Locale | Null = null) = + diagnostics.asScala.map(_.getMessage(locale)).toList + + def reportable(): String = + import scala.util.Properties.lineSeparator + clear() + if messages.nonEmpty then messages.mkString("", lineSeparator, lineSeparator) + else "" + end JavaReporter + + val reporter = new JavaReporter + + // javax.tools.DisassemblerTool.getStandardFileManager(reporter, locale, charset) + val defaultFileManager: JavaFileManager = + (loader + .tryToLoadClass[JavaFileManager]("com.sun.tools.javap.JavapFileManager").get + .getMethod("create", classOf[DiagnosticListener[?]], classOf[PrintWriter]).nn + .invoke(null, reporter, PrintWriter(System.err, true)) + ).asInstanceOf[JavaFileManager] + + // manages named arrays of bytes, which might have failed to load + class JavapFileManager(val managed: Seq[Input])(delegate: JavaFileManager = defaultFileManager) + extends ForwardingJavaFileManager[JavaFileManager](delegate): + import JavaFileObject.Kind + import Kind.* + import StandardLocation.* + import JavaFileManager.Location + import java.net.{URI, URISyntaxException} + import java.io.{ByteArrayInputStream, InputStream} + + // name#fragment is OK, but otherwise fragile + def uri(name: String): URI = + try URI(name) // URI("jfo:" + name) + catch case _: URISyntaxException => URI("dummy") + + // look up by actual class name or by target descriptor (unused?) + def inputNamed(name: String): Try[Array[Byte]] = + managed.find(m => m.actual == name || m.target == name).get.data + + def managedFile(name: String, kind: Kind) = kind match + case CLASS => fileObjectForInput(name, inputNamed(name), kind) + case _ => null + + // todo: just wrap it as scala abstractfile and adapt it uniformly + def fileObjectForInput(name: String, bytes: Try[Array[Byte]], kind: Kind): JavaFileObject = + new SimpleJavaFileObject(uri(name), kind): + override def openInputStream(): InputStream = ByteArrayInputStream(bytes.get) + // if non-null, ClassWriter wrongly requires scheme non-null + override def toUri: URI | Null = null + override def getName: String = name + // suppress + override def getLastModified: Long = -1L + end fileObjectForInput + + override def getJavaFileForInput(location: Location, className: String, kind: Kind): JavaFileObject | Null = + location match + case CLASS_PATH => managedFile(className, kind) + case _ => null + + override def hasLocation(location: Location): Boolean = + location match + case CLASS_PATH => true + case _ => false + end JavapFileManager + + def fileManager(inputs: Seq[Input]) = JavapFileManager(inputs)() + + // ServiceLoader.load(classOf[javax.tools.DisassemblerTool]) + // .getTask(writer, fileManager, reporter, options.asJava, classes.asJava) + def task(options: Seq[String], classes: Seq[String], inputs: Seq[Input]): Task = + loader.createInstance[Task] + (JavapTask.taskClassName, Console.println(_)) + (writer, fileManager(inputs), reporter, options.asJava, classes.asJava) + + /** Run the tool. */ + override def apply(options: Seq[String])(inputs: Seq[Input]): List[DisResult] = + def runInput(input: Input): DisResult = input match + case Input(target, actual, Success(_)) => + import java.lang.reflect.InvocationTargetException + try + if task(options, Seq(actual), inputs).call() then + DisSuccess(target, reporter.reportable() + written()) + else + DisError(reporter.reportable()) + catch case e: InvocationTargetException => + e.getCause match + case t: IllegalArgumentException => DisError(t.getMessage) // bad option + case t: Throwable => throw t + case null => throw e + finally + reporter.clear() + + case Input(_, _, Failure(e)) => + DisError(e.getMessage) + end runInput + + inputs.map(runInput).toList + end apply + +object JavapTask: + // introduced in JDK7 as internal API + val taskClassName = "com.sun.tools.javap.JavapTask" +end JavapTask diff --git a/compiler/src/dotty/tools/repl/ParseResult.scala b/compiler/src/dotty/tools/repl/ParseResult.scala index effd7740517f..50ba12d3c925 100644 --- a/compiler/src/dotty/tools/repl/ParseResult.scala +++ b/compiler/src/dotty/tools/repl/ParseResult.scala @@ -52,6 +52,12 @@ object Load { val command: String = ":load" } + +/** Run the javap disassembler on the given target(s) */ +case class JavapOf(args: String) extends Command +object JavapOf: + val command: String = ":javap" + /** To find out the type of an expression you may simply do: * * ``` @@ -113,6 +119,7 @@ case object Help extends Command { |:imports show import history |:reset [options] reset the repl to its initial state, forgetting all session entries |:settings update compiler options, if possible + |:javap disassemble a file or class name """.stripMargin } @@ -137,6 +144,7 @@ object ParseResult { TypeOf.command -> (arg => TypeOf(arg)), DocOf.command -> (arg => DocOf(arg)), Settings.command -> (arg => Settings(arg)), + JavapOf.command -> (arg => JavapOf(arg)) ) def apply(source: SourceFile)(implicit state: State): ParseResult = { diff --git a/compiler/src/dotty/tools/repl/ReplDriver.scala b/compiler/src/dotty/tools/repl/ReplDriver.scala index fe1bba9317f5..bd4701433f46 100644 --- a/compiler/src/dotty/tools/repl/ReplDriver.scala +++ b/compiler/src/dotty/tools/repl/ReplDriver.scala @@ -462,6 +462,12 @@ class ReplDriver(settings: Array[String], state } + case JavapOf(line) => + given DisassemblerRepl(this, state) + val opts = JavapOptions.parse(ReplStrings.words(line)) + disassemble(Javap, opts) + state + case TypeOf(expr) => expr match { case "" => out.println(s":type ") diff --git a/compiler/src/dotty/tools/runner/ScalaClassLoader.scala b/compiler/src/dotty/tools/runner/ScalaClassLoader.scala index 30ba62701191..4f1d5bc7f855 100644 --- a/compiler/src/dotty/tools/runner/ScalaClassLoader.scala +++ b/compiler/src/dotty/tools/runner/ScalaClassLoader.scala @@ -10,6 +10,7 @@ import java.net.{URL, URLClassLoader} import scala.annotation.internal.sharable import scala.annotation.tailrec +import scala.reflect.{ClassTag, classTag} import scala.util.control.Exception.catching object ClassLoaderOps: @@ -24,6 +25,32 @@ object ClassLoaderOps: action finally setContext(saved) + /** Create an instance with ctor args, or invoke errorFn before throwing. */ + def createInstance[T <: AnyRef : ClassTag](path: String, errorFn: String => Unit)(args: AnyRef*): T = + def fail(msg: String) = error(msg, new IllegalArgumentException(msg)) + def error(msg: String, e: Throwable) = { errorFn(msg) ; throw e } + try + val clazz = Class.forName(path, /*initialize =*/ true, /*loader =*/ self) + if classTag[T].runtimeClass.isAssignableFrom(clazz) then + val ctor = + val maybes = clazz.getConstructors.filter(c => + c.getParameterCount == args.size + && (c.getParameterTypes zip args).forall { case (k, a) => k.isAssignableFrom(a.getClass) }) + if maybes.size == 1 then maybes.head + else fail(s"Constructor must accept arg list (${args.map(_.getClass.getName).mkString(", ")}): ${path}") + (ctor.newInstance(args*)).asInstanceOf[T] + else + // TODO show is undefined; in the original Scala 2 code it is imported from + // import scala.reflect.runtime.ReflectionUtils.show + //errorFn(s"""Loader for ${classTag[T]}: [${show(classTag[T].runtimeClass.getClassLoader)}] + // |Loader for ${clazz.getName}: [${show(clazz.getClassLoader)}]""".stripMargin) + fail(s"Not a ${classTag[T]}: ${path}") + catch + case e: ClassNotFoundException => + error(s"Class not found: ${path}", e) + case e @ (_: LinkageError | _: ReflectiveOperationException) => + error(s"Unable to create instance: ${path}: ${e.toString}", e) + /** Load and link a class with this classloader */ def tryToLoadClass[T <: AnyRef](path: String): Option[Class[T]] = tryClass(path, initialize = false) diff --git a/compiler/test/dotty/tools/repl/DisassemblerTests.scala b/compiler/test/dotty/tools/repl/DisassemblerTests.scala index 307fecd914c4..022ccda98b19 100644 --- a/compiler/test/dotty/tools/repl/DisassemblerTests.scala +++ b/compiler/test/dotty/tools/repl/DisassemblerTests.scala @@ -248,3 +248,50 @@ abstract class DisassemblerTest extends ReplDisassemblerTest: assert(!out.linesIterator.exists(_.contains(line)), s"disassembly unexpectedly contained `$line`") end DisassemblerTest + +// Test disassembly using `:javap` +class JavapTests extends DisassemblerTest: + override val packageSeparator = "." + + @Test def `simple end-to-end` = + eval("class Foo1").andThen { + run(":javap -c Foo1") + assertDisassemblyIncludes(s"public class ${line(1, "Foo1")} {") + } + + @Test def `multiple classes in prev entry` = + eval { + """class Foo2 + |trait Bar2 + |""".stripMargin + } andThen { + run(":javap -c -") + assertDisassemblyIncludes(List( + s"public class ${line(1, "Foo2")} {", + s"public interface ${line(1, "Bar2")} {", + )) + } + + @Test def `private selected method` = + eval { + """class Baz1: + | private def one = 1 + | private def two = 2 + |""".stripMargin + } andThen { + run(":javap -p -c Baz1#one") + val out = storedOutput() + assertDisassemblyIncludes("private int one();", out) + assertDisassemblyExcludes("private int two();", out) + } + + @Test def `java.lang.String signatures` = + initially { + run(":javap -s java.lang.String") + val out = storedOutput() + assertDisassemblyIncludes("public static java.lang.String format(java.lang.String, java.lang.Object...);", out) + assertDisassemblyIncludes("public static java.lang.String join(java.lang.CharSequence, java.lang.Iterable);", out) + assertDisassemblyIncludes("public java.lang.String concat(java.lang.String);", out) + assertDisassemblyIncludes("public java.lang.String trim();", out) + } +end JavapTests diff --git a/compiler/test/dotty/tools/repl/TabcompleteTests.scala b/compiler/test/dotty/tools/repl/TabcompleteTests.scala index ecb01d6863da..56b3cbe32bad 100644 --- a/compiler/test/dotty/tools/repl/TabcompleteTests.scala +++ b/compiler/test/dotty/tools/repl/TabcompleteTests.scala @@ -211,6 +211,7 @@ class TabcompleteTests extends ReplTest { ":exit", ":help", ":imports", + ":javap", ":load", ":quit", ":reset", From 96ba466532a0346975fa92b7eb32b9463632ebe7 Mon Sep 17 00:00:00 2001 From: Tom Grigg Date: Thu, 17 Mar 2022 19:46:07 -0700 Subject: [PATCH 4/6] tests for Javap filter selection --- .../dotty/tools/repl/DisassemblerTests.scala | 193 ++++++++++++++++++ 1 file changed, 193 insertions(+) diff --git a/compiler/test/dotty/tools/repl/DisassemblerTests.scala b/compiler/test/dotty/tools/repl/DisassemblerTests.scala index 022ccda98b19..8c0fb5d0af39 100644 --- a/compiler/test/dotty/tools/repl/DisassemblerTests.scala +++ b/compiler/test/dotty/tools/repl/DisassemblerTests.scala @@ -295,3 +295,196 @@ class JavapTests extends DisassemblerTest: assertDisassemblyIncludes("public java.lang.String trim();", out) } end JavapTests + +// Test output filters +class JavapFilterSelectionTests: + // test -sysinfo disassembly + private val listSysinfo = + """ Size 51190 bytes + | MD5 checksum fa1f9a810f5fff1bac4c3d1ae2051ab5 + | Compiled from "List.scala" + |public abstract class scala.collection.immutable.List extends scala.collection.immutable.AbstractSeq implements scala.collection.immutable.LinearSeq, scala.collection.StrictOptimizedLinearSeqOps>, scala.collection.immutable.StrictOptimizedSeqOps>, scala.collection.generic.DefaultSerializable { + | public static scala.collection.SeqOps unapplySeq(scala.collection.SeqOps); + | public scala.collection.LinearSeq drop(int); + | public scala.collection.LinearSeq dropWhile(scala.Function1); + | public java.lang.Object drop(int); + | public java.lang.Object sorted(scala.math.Ordering); + |} + """.stripMargin + + @Test + def `select drop from listSysinfo`: Unit = + assertEquals( + """| public scala.collection.LinearSeq drop(int); + | public java.lang.Object drop(int); + |""".stripMargin, + Javap.filterSelection("List#drop")(listSysinfo)) + + // test -l disassembly + private val listL = + """Compiled from "List.scala" + |public abstract class scala.collection.immutable.List extends scala.collection.immutable.AbstractSeq implements scala.collection.immutable.LinearSeq, scala.collection.StrictOptimizedLinearSeqOps>, scala.collection.immutable.StrictOptimizedSeqOps>, scala.collection.generic.DefaultSerializable { + | public static scala.collection.SeqOps unapplySeq(scala.collection.SeqOps); + | LineNumberTable: + | line 648: 4 + | + | public scala.collection.LinearSeq drop(int); + | LineNumberTable: + | line 79: 0 + | LocalVariableTable: + | Start Length Slot Name Signature + | 0 6 0 this Lscala/collection/immutable/List; + | 0 6 1 n I + | + | public scala.collection.LinearSeq dropWhile(scala.Function1); + | LineNumberTable: + | line 79: 0 + | LocalVariableTable: + | Start Length Slot Name Signature + | 0 6 0 this Lscala/collection/immutable/List; + | 0 6 1 p Lscala/Function1; + | + | public java.lang.Object drop(int); + | LineNumberTable: + | line 79: 0 + | LocalVariableTable: + | Start Length Slot Name Signature + | 0 6 0 this Lscala/collection/immutable/List; + | 0 6 1 n I + | + | public java.lang.Object sorted(scala.math.Ordering); + | LineNumberTable: + | line 79: 0 + | LocalVariableTable: + | Start Length Slot Name Signature + | 0 6 0 this Lscala/collection/immutable/List; + | 0 6 1 ord Lscala/math/Ordering; + |} + """.stripMargin + + @Test + def `select drop from listL`: Unit = + assertEquals( + """| public scala.collection.LinearSeq drop(int); + | LineNumberTable: + | line 79: 0 + | LocalVariableTable: + | Start Length Slot Name Signature + | 0 6 0 this Lscala/collection/immutable/List; + | 0 6 1 n I + | public java.lang.Object drop(int); + | LineNumberTable: + | line 79: 0 + | LocalVariableTable: + | Start Length Slot Name Signature + | 0 6 0 this Lscala/collection/immutable/List; + | 0 6 1 n I + |""".stripMargin, + Javap.filterSelection("List#drop")(listL)) + + // test -v disassembly // TODO implement + private val listV = + """| + """.stripMargin + + // test -s disassembly + private val listS = + """Compiled from "List.scala" + |public abstract class scala.collection.immutable.List extends scala.collection.immutable.AbstractSeq implements scala.collection.immutable.LinearSeq, scala.collection.StrictOptimizedLinearSeqOps>, scala.collection.immutable.StrictOptimizedSeqOps>, scala.collection.generic.DefaultSerializable { + | public static scala.collection.SeqOps unapplySeq(scala.collection.SeqOps); + | descriptor: (Lscala/collection/SeqOps;)Lscala/collection/SeqOps; + | + | public scala.collection.LinearSeq drop(int); + | descriptor: (I)Lscala/collection/LinearSeq; + | + | public scala.collection.LinearSeq dropWhile(scala.Function1); + | descriptor: (Lscala/Function1;)Lscala/collection/LinearSeq; + | + | public java.lang.Object drop(int); + | descriptor: (I)Ljava/lang/Object; + | + | public java.lang.Object sorted(scala.math.Ordering); + | descriptor: (Lscala/math/Ordering;)Ljava/lang/Object; + |} + """.stripMargin + + @Test + def `select drop from listS`: Unit = + assertEquals( + """| public scala.collection.LinearSeq drop(int); + | descriptor: (I)Lscala/collection/LinearSeq; + | public java.lang.Object drop(int); + | descriptor: (I)Ljava/lang/Object; + |""".stripMargin, + Javap.filterSelection("List#drop")(listS)) + + private val listC = + """Compiled from "List.scala" + |public abstract class scala.collection.immutable.List extends scala.collection.immutable.AbstractSeq implements scala.collection.immutable.LinearSeq, scala.collection.StrictOptimizedLinearSeqOps>, scala.collection.immutable.StrictOptimizedSeqOps>, scala.collection.generic.DefaultSerializable { + | public static scala.collection.SeqOps unapplySeq(scala.collection.SeqOps); + | Code: + | 0: getstatic #43 // Field scala/collection/immutable/List$.MODULE$:Lscala/collection/immutable/List$; + | 3: pop + | 4: aload_0 + | 5: areturn + | + | public scala.collection.LinearSeq drop(int); + | Code: + | 0: aload_0 + | 1: iload_1 + | 2: invokestatic #223 // InterfaceMethod scala/collection/StrictOptimizedLinearSeqOps.drop$:(Lscala/collection/StrictOptimizedLinearSeqOps;I)Lscala/collection/LinearSeq; + | 5: areturn + | + | public scala.collection.LinearSeq dropWhile(scala.Function1); + | Code: + | 0: aload_0 + | 1: aload_1 + | 2: invokestatic #230 // InterfaceMethod scala/collection/StrictOptimizedLinearSeqOps.dropWhile$:(Lscala/collection/StrictOptimizedLinearSeqOps;Lscala/Function1;)Lscala/collection/LinearSeq; + | 5: areturn + | + | public java.lang.Object drop(int); + | Code: + | 0: aload_0 + | 1: iload_1 + | 2: invokevirtual #792 // Method drop:(I)Lscala/collection/LinearSeq; + | 5: areturn + | + | public java.lang.Object sorted(scala.math.Ordering); + | Code: + | 0: aload_0 + | 1: aload_1 + | 2: invokestatic #210 // InterfaceMethod scala/collection/immutable/StrictOptimizedSeqOps.sorted$:(Lscala/collection/immutable/StrictOptimizedSeqOps;Lscala/math/Ordering;)Ljava/lang/Object; + | 5: areturn + |} + """.stripMargin + + @Test + def `select drop from List disassembly`: Unit = + assertEquals( + """| public scala.collection.LinearSeq drop(int); + | Code: + | 0: aload_0 + | 1: iload_1 + | 2: invokestatic #223 // InterfaceMethod scala/collection/StrictOptimizedLinearSeqOps.drop$:(Lscala/collection/StrictOptimizedLinearSeqOps;I)Lscala/collection/LinearSeq; + | 5: areturn + | public java.lang.Object drop(int); + | Code: + | 0: aload_0 + | 1: iload_1 + | 2: invokevirtual #792 // Method drop:(I)Lscala/collection/LinearSeq; + | 5: areturn + |""".stripMargin, + Javap.filterSelection("List#drop")(listC)) + + @Test + def `select last method from disassembly`: Unit = + assertEquals( + """| public java.lang.Object sorted(scala.math.Ordering); + | Code: + | 0: aload_0 + | 1: aload_1 + | 2: invokestatic #210 // InterfaceMethod scala/collection/immutable/StrictOptimizedSeqOps.sorted$:(Lscala/collection/immutable/StrictOptimizedSeqOps;Lscala/math/Ordering;)Ljava/lang/Object; + | 5: areturn + |""".stripMargin, + Javap.filterSelection("List#sorted")(listC)) +end JavapFilterSelectionTests From 4883a45d3d6d2bddbc84a5a46df7020215231e96 Mon Sep 17 00:00:00 2001 From: Tom Grigg Date: Thu, 17 Mar 2022 19:46:07 -0700 Subject: [PATCH 5/6] Add `:asmp` to the REPL Provides bytecode disassembly using the ASM library bundled with the Scala compiler. --- .../src/dotty/tools/repl/Disassembler.scala | 206 ++++++++++++++++++ .../src/dotty/tools/repl/ParseResult.scala | 6 + .../src/dotty/tools/repl/ReplDriver.scala | 6 + .../dotty/tools/repl/DisassemblerTests.scala | 51 +++++ .../dotty/tools/repl/TabcompleteTests.scala | 1 + 5 files changed, 270 insertions(+) diff --git a/compiler/src/dotty/tools/repl/Disassembler.scala b/compiler/src/dotty/tools/repl/Disassembler.scala index eec2fb471a5e..70549d010f85 100644 --- a/compiler/src/dotty/tools/repl/Disassembler.scala +++ b/compiler/src/dotty/tools/repl/Disassembler.scala @@ -3,6 +3,7 @@ package repl import scala.annotation.internal.sharable import scala.util.{Failure, Success, Try} +import scala.util.control.NonFatal import scala.util.matching.Regex import dotc.core.StdNames.* @@ -627,3 +628,208 @@ object JavapTask: // introduced in JDK7 as internal API val taskClassName = "com.sun.tools.javap.JavapTask" end JavapTask + +/** A disassembler implemented using the ASM library (a dependency of the backend) + * Supports flags similar to javap, with some additions and omissions. + */ +object Asmp extends Disassembler: + import Disassembler.* + + def apply(opts: DisassemblerOptions)(using repl: DisassemblerRepl): List[DisResult] = + val tool = AsmpTool() + val clazz = DisassemblyClass(repl.classLoader) + tool(opts.flags)(opts.targets.map(clazz.bytes(_))) + + // The flags are intended to resemble those used by javap + val helps = List( + "usage" -> ":asmp [opts] [path or class or -]...", + "-help" -> "Prints this help message", + "-verbose/-v" -> "Stack size, number of locals, method args", + "-private/-p" -> "Private classes and members", + "-package" -> "Package-private classes and members", + "-protected" -> "Protected classes and members", + "-public" -> "Public classes and members", + "-c" -> "Disassembled code", + "-s" -> "Internal type signatures", + "-filter" -> "Filter REPL machinery from output", + "-raw" -> "Don't post-process output from ASM", // TODO for debugging + "-decls" -> "Declarations", + "-bridges" -> "Bridges", + "-synthetics" -> "Synthetics", + ) + + override def filters(target: String, opts: DisassemblerOptions): List[String => String] = + val commonFilters = super.filters(target, opts) + if opts.flags.contains("-decls") then filterCommentsBlankLines :: commonFilters + else squashConsectiveBlankLines :: commonFilters // default filters + + // A filter to compress consecutive blank lines into a single blank line + private def squashConsectiveBlankLines(s: String) = s.replaceAll("\n{3,}", "\n\n").nn + + // A filter to remove all blank lines and lines beginning with "//" + private def filterCommentsBlankLines(s: String): String = + val comment = raw"\s*// .*".r + def isBlankLine(s: String) = s.trim == "" + def isComment(s: String) = comment.matches(s) + filteredLines(s, t => !isComment(t) && !isBlankLine(t)) +end Asmp + +object AsmpOptions extends DisassemblerOptionParser(Asmp.helps): + val defaultToolOptions = List("-protected", "-verbose") + +/** Implementation of the ASM-based disassembly tool. */ +class AsmpTool extends DisassemblyTool: + import DisassemblyTool.* + import Disassembler.splitHashMember + import java.io.{PrintWriter, StringWriter} + import scala.tools.asm.{Attribute, ClassReader, Label, Opcodes} + import scala.tools.asm.util.{Textifier, TraceClassVisitor} + import dotty.tools.backend.jvm.ClassNode1 + + enum Mode: + case Verbose, Code, Signatures + + /** A Textifier subclass to control the disassembly output based on flags. + * The visitor methods overriden here conditionally suppress their output + * based on the flags and targets supplied to the disassembly tool. + * + * The filtering performed falls into three categories: + * - operating mode: -verbose, -c, -s, etc. + * - access flags: -protected, -private, -public, etc. + * - member name: e.g. a target given as Klass#method + * + * This is all bypassed if the `-raw` flag is given. + */ + class FilteringTextifier(mode: Mode, accessFilter: Int => Boolean, nameFilter: Option[String]) + extends Textifier(Opcodes.ASM9): + private def keep(access: Int, name: String): Boolean = + accessFilter(access) && nameFilter.map(_ == name).getOrElse(true) + + override def visitField(access: Int, name: String, descriptor: String, signature: String, value: Any): Textifier = + if keep(access, name) then + super.visitField(access, name, descriptor, signature, value) + addNewTextifier(discard = (mode == Mode.Signatures)) + else + addNewTextifier(discard = true) + + override def visitMethod(access:Int, name: String, descriptor: String, signature: String, exceptions: Array[String | Null]): Textifier = + if keep(access, name) then + super.visitMethod(access, name, descriptor, signature, exceptions) + addNewTextifier(discard = (mode == Mode.Signatures)) + else + addNewTextifier(discard = true) + + override def visitInnerClass(name: String, outerName: String, innerName: String, access: Int): Unit = + if mode == Mode.Verbose && keep(access, name) then + super.visitInnerClass(name, outerName, innerName, access) + + override def visitClassAttribute(attribute: Attribute): Unit = + if mode == Mode.Verbose && nameFilter.isEmpty then + super.visitClassAttribute(attribute) + + override def visitClassAnnotation(descriptor: String, visible: Boolean): Textifier | Null = + // suppress ScalaSignature unless -raw given. Should we? TODO + if mode == Mode.Verbose && nameFilter.isEmpty && descriptor != "Lscala/reflect/ScalaSignature;" then + super.visitClassAnnotation(descriptor, visible) + else + addNewTextifier(discard = true) + + override def visitSource(file: String, debug: String): Unit = + if mode == Mode.Verbose && nameFilter.isEmpty then + super.visitSource(file, debug) + + override def visitAnnotation(descriptor: String, visible: Boolean): Textifier | Null = + if mode == Mode.Verbose then + super.visitAnnotation(descriptor, visible) + else + addNewTextifier(discard = true) + + override def visitLineNumber(line: Int, start: Label): Unit = + if mode == Mode.Verbose then + super.visitLineNumber(line, start) + + override def visitMaxs(maxStack: Int, maxLocals: Int): Unit = + if mode == Mode.Verbose then + super.visitMaxs(maxStack, maxLocals) + + override def visitLocalVariable(name: String, descriptor: String, signature: String, start: Label, end: Label, index: Int): Unit = + if mode == Mode.Verbose then + super.visitLocalVariable(name, descriptor, signature, start, end, index) + + private def isLabel(s: String) = raw"\s*L\d+\s*".r.matches(s) + + // ugly hack to prevent orphaned label when local vars, max stack not displayed (e.g. in -c mode) + override def visitMethodEnd(): Unit = if text != null then text.size match + case 0 => + case n => + if isLabel(text.get(n - 1).toString) then + try text.remove(n - 1) + catch case _: UnsupportedOperationException => () + + private def addNewTextifier(discard: Boolean = false): Textifier = + val tx = FilteringTextifier(mode, accessFilter, nameFilter) + if !discard then text.nn.add(tx.getText()) + tx + end FilteringTextifier + + override def apply(options: Seq[String])(inputs: Seq[Input]): List[DisResult] = + def parseMode(opts: Seq[String]): Mode = + if opts.contains("-c") then Mode.Code + else if opts.contains("-s") || opts.contains("-decls") then Mode.Signatures + else Mode.Verbose // default + + def parseAccessLevel(opts: Seq[String]): Int = + if opts.contains("-public") then Opcodes.ACC_PUBLIC + else if opts.contains("-protected") then Opcodes.ACC_PROTECTED + else if opts.contains("-private") || opts.contains("-p") then Opcodes.ACC_PRIVATE + else 0 + + def accessFilter(mode: Mode, accessLevel: Int, opts: Seq[String]): Int => Boolean = + inline def contains(mask: Int) = (a: Int) => (a & mask) != 0 + inline def excludes(mask: Int) = (a: Int) => (a & mask) == 0 + val showSynthetics = opts.contains("-synthetics") + val showBridges = opts.contains("-bridges") + def accessible: Int => Boolean = accessLevel match + case Opcodes.ACC_PUBLIC => contains(Opcodes.ACC_PUBLIC) + case Opcodes.ACC_PROTECTED => contains(Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED) + case Opcodes.ACC_PRIVATE => _ => true + case _ /* package */ => excludes(Opcodes.ACC_PRIVATE) + def included(access: Int): Boolean = mode match + case Mode.Verbose => true + case _ => + val isBridge = contains(Opcodes.ACC_BRIDGE)(access) + val isSynthetic = contains(Opcodes.ACC_SYNTHETIC)(access) + if isSynthetic && showSynthetics then true // TODO do we have tests for -synthetics? + else if isBridge && showBridges then true // TODO do we have tests for -bridges? + else if isSynthetic || isBridge then false + else true + a => accessible(a) && included(a) + + def runInput(input: Input): DisResult = input match + case Input(target, _, Success(bytes)) => + val sw = StringWriter() + val pw = PrintWriter(sw) + val node = ClassNode1() + + val tx = + if options.contains("-raw") then + Textifier() + else + val mode = parseMode(options) + val accessLevel = parseAccessLevel(options) + val nameFilter = splitHashMember(target).map(s => if s.isEmpty then "apply" else s) + FilteringTextifier(mode, accessFilter(mode, accessLevel, options), nameFilter) + + try + ClassReader(bytes).accept(node, 0) + node.accept(TraceClassVisitor(null, tx, pw)) + pw.flush() + DisSuccess(target, sw.toString) + catch case NonFatal(e) => DisError(e.getMessage) + case Input(_, _, Failure(e)) => + DisError(e.getMessage) + end runInput + + inputs.map(runInput).toList + end apply +end AsmpTool diff --git a/compiler/src/dotty/tools/repl/ParseResult.scala b/compiler/src/dotty/tools/repl/ParseResult.scala index 50ba12d3c925..d23cdd1a80fd 100644 --- a/compiler/src/dotty/tools/repl/ParseResult.scala +++ b/compiler/src/dotty/tools/repl/ParseResult.scala @@ -52,6 +52,10 @@ object Load { val command: String = ":load" } +/** Run the ASM based disassembler on the given target(s) */ +case class AsmpOf(args: String) extends Command +object AsmpOf: + val command: String = ":asmp" /** Run the javap disassembler on the given target(s) */ case class JavapOf(args: String) extends Command @@ -119,6 +123,7 @@ case object Help extends Command { |:imports show import history |:reset [options] reset the repl to its initial state, forgetting all session entries |:settings update compiler options, if possible + |:asmp disassemble a file or class name (experimental) |:javap disassemble a file or class name """.stripMargin } @@ -144,6 +149,7 @@ object ParseResult { TypeOf.command -> (arg => TypeOf(arg)), DocOf.command -> (arg => DocOf(arg)), Settings.command -> (arg => Settings(arg)), + AsmpOf.command -> (arg => AsmpOf(arg)), JavapOf.command -> (arg => JavapOf(arg)) ) diff --git a/compiler/src/dotty/tools/repl/ReplDriver.scala b/compiler/src/dotty/tools/repl/ReplDriver.scala index bd4701433f46..e71b1861f64d 100644 --- a/compiler/src/dotty/tools/repl/ReplDriver.scala +++ b/compiler/src/dotty/tools/repl/ReplDriver.scala @@ -462,6 +462,12 @@ class ReplDriver(settings: Array[String], state } + case AsmpOf(line) => + given DisassemblerRepl(this, state) + val opts = AsmpOptions.parse(ReplStrings.words(line)) + disassemble(Asmp, opts) + state + case JavapOf(line) => given DisassemblerRepl(this, state) val opts = JavapOptions.parse(ReplStrings.words(line)) diff --git a/compiler/test/dotty/tools/repl/DisassemblerTests.scala b/compiler/test/dotty/tools/repl/DisassemblerTests.scala index 8c0fb5d0af39..8e5a9cc8606a 100644 --- a/compiler/test/dotty/tools/repl/DisassemblerTests.scala +++ b/compiler/test/dotty/tools/repl/DisassemblerTests.scala @@ -488,3 +488,54 @@ class JavapFilterSelectionTests: |""".stripMargin, Javap.filterSelection("List#sorted")(listC)) end JavapFilterSelectionTests + +// Test disassembly using `:asmp` +class AsmpTests extends DisassemblerTest: + override val packageSeparator = "/" + + @Test def `simple end-to-end` = + eval("class Foo1").andThen { + run(":asmp -c Foo1") + assertDisassemblyIncludes(List( + s"public class ${line(1, "Foo1")} {", + "public ()V", + "INVOKESPECIAL java/lang/Object. ()V", + )) + } + + @Test def `multiple classes in prev entry` = + eval { + """class Foo2 + |trait Bar2 + |""".stripMargin + } andThen { + run(":asmp -c -") + assertDisassemblyIncludes(List( + s"public class ${line(1, "Foo2")} {", + s"public abstract interface ${line(1, "Bar2")} {", + )) + } + + @Test def `private selected method` = + eval { + """class Baz1: + | private def one = 1 + | private def two = 2 + |""".stripMargin + } andThen { + run(":asmp -p -c Baz1#one") + val out = storedOutput() + assertDisassemblyIncludes("private one()I", out) + assertDisassemblyExcludes("private two()I", out) + } + + @Test def `java.lang.String signatures` = + initially { + run(":asmp -s java.lang.String") + val out = storedOutput() + assertDisassemblyIncludes("public static varargs format(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;", out) + assertDisassemblyIncludes("public static join(Ljava/lang/CharSequence;Ljava/lang/Iterable;)Ljava/lang/String;", out) + assertDisassemblyIncludes("public concat(Ljava/lang/String;)Ljava/lang/String;", out) + assertDisassemblyIncludes("public trim()Ljava/lang/String;", out) + } +end AsmpTests diff --git a/compiler/test/dotty/tools/repl/TabcompleteTests.scala b/compiler/test/dotty/tools/repl/TabcompleteTests.scala index 56b3cbe32bad..7c695f0cc10a 100644 --- a/compiler/test/dotty/tools/repl/TabcompleteTests.scala +++ b/compiler/test/dotty/tools/repl/TabcompleteTests.scala @@ -207,6 +207,7 @@ class TabcompleteTests extends ReplTest { @Test def commands = initially { assertEquals( List( + ":asmp", ":doc", ":exit", ":help", From fcfbe987bfdf712f6dcb7833f7d55ddb9c0e602a Mon Sep 17 00:00:00 2001 From: Tom Grigg Date: Thu, 17 Mar 2022 19:46:07 -0700 Subject: [PATCH 6/6] [do not merge] test on additional JDKs --- .github/workflows/ci.yaml | 210 ++++++++++++++++++ .../dotty/tools/dotc/profile/Profiler.scala | 1 + library/src/scala/runtime/LazyVals.scala | 1 + 3 files changed, 212 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6cad2057dd01..e91f55761037 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -461,6 +461,216 @@ jobs: ./project/scripts/cmdTests ./project/scripts/bootstrappedOnlyCmdTests + test_java11: + runs-on: [self-hosted, Linux] + container: + image: lampepfl/dotty:2021-03-22 + options: --cpu-shares 4096 + volumes: + - ${{ github.workspace }}/../../cache/sbt:/root/.sbt + - ${{ github.workspace }}/../../cache/ivy:/root/.ivy2/cache + - ${{ github.workspace }}/../../cache/general:/root/.cache + + if: "( + github.event_name == 'pull_request' + && !contains(github.event.pull_request.body, '[skip ci]') + && contains(github.event.pull_request.body, '[test_java11]') + )" + + steps: + - name: Install JDK 11 + run: apt-get update && apt-get install -y openjdk-11-jdk-headless + + - name: Set JDK 11 as default + run: echo "/usr/lib/jvm/java-11-openjdk-amd64/bin" >> $GITHUB_PATH + + - name: Reset existing repo + run: git -c "http.https://github.com/.extraheader=" fetch --recurse-submodules=no "https://github.com/lampepfl/dotty" && git reset --hard FETCH_HEAD || true + + - name: Checkout cleanup script + uses: actions/checkout@v2 + + - name: Cleanup + run: .github/workflows/cleanup.sh + + - name: Git Checkout + uses: actions/checkout@v2 + + - name: Add SBT proxy repositories + run: cp -vf .github/workflows/repositories /root/.sbt/ ; true + + - name: Test + run: | + ./project/scripts/sbt "scala3-bootstrapped/compile; scala3-bootstrapped/testOnly dotty.tools.repl.Disassemb* dotty.tools.repl.Javap* dotty.tools.repl.Asmp*" + + test_java15: + runs-on: [self-hosted, Linux] + container: + image: lampepfl/dotty:2021-03-22 + options: --cpu-shares 4096 + volumes: + - ${{ github.workspace }}/../../cache/sbt:/root/.sbt + - ${{ github.workspace }}/../../cache/ivy:/root/.ivy2/cache + - ${{ github.workspace }}/../../cache/general:/root/.cache + + if: "( + github.event_name == 'pull_request' + && !contains(github.event.pull_request.body, '[skip ci]') + && contains(github.event.pull_request.body, '[test_java15]') + )" + + steps: + - name: Install JDK 15 + run: apt-get update && apt-get install -y openjdk-15-jdk-headless + + - name: Set JDK 15 as default + run: echo "/usr/lib/jvm/java-15-openjdk-amd64/bin" >> $GITHUB_PATH + + - name: Reset existing repo + run: git -c "http.https://github.com/.extraheader=" fetch --recurse-submodules=no "https://github.com/lampepfl/dotty" && git reset --hard FETCH_HEAD || true + + - name: Checkout cleanup script + uses: actions/checkout@v2 + + - name: Cleanup + run: .github/workflows/cleanup.sh + + - name: Git Checkout + uses: actions/checkout@v2 + + - name: Add SBT proxy repositories + run: cp -vf .github/workflows/repositories /root/.sbt/ ; true + + - name: Test + run: | + ./project/scripts/sbt "scala3-bootstrapped/compile; scala3-bootstrapped/testOnly dotty.tools.repl.Disassemb* dotty.tools.repl.Javap* dotty.tools.repl.Asmp*" + + test_java17: + runs-on: [self-hosted, Linux] + container: + image: lampepfl/dotty:2021-03-22 + options: --cpu-shares 4096 + volumes: + - ${{ github.workspace }}/../../cache/sbt:/root/.sbt + - ${{ github.workspace }}/../../cache/ivy:/root/.ivy2/cache + - ${{ github.workspace }}/../../cache/general:/root/.cache + + if: "( + github.event_name == 'pull_request' + && !contains(github.event.pull_request.body, '[skip ci]') + && contains(github.event.pull_request.body, '[test_java17]') + )" + + steps: + - name: Install JDK 17 + run: apt-get update && apt-get install -y openjdk-17-jdk-headless + + - name: Set JDK 17 as default + run: echo "/usr/lib/jvm/java-17-openjdk-amd64/bin" >> $GITHUB_PATH + + - name: Reset existing repo + run: git -c "http.https://github.com/.extraheader=" fetch --recurse-submodules=no "https://github.com/lampepfl/dotty" && git reset --hard FETCH_HEAD || true + + - name: Checkout cleanup script + uses: actions/checkout@v2 + + - name: Cleanup + run: .github/workflows/cleanup.sh + + - name: Git Checkout + uses: actions/checkout@v2 + + - name: Add SBT proxy repositories + run: cp -vf .github/workflows/repositories /root/.sbt/ ; true + + - name: Test + run: | + ./project/scripts/sbt "scala3-bootstrapped/compile; scala3-bootstrapped/testOnly dotty.tools.repl.Disassemb* dotty.tools.repl.Javap* dotty.tools.repl.Asmp*" + + test_java18: + runs-on: [self-hosted, Linux] + container: + image: lampepfl/dotty:2021-03-22 + options: --cpu-shares 4096 + volumes: + - ${{ github.workspace }}/../../cache/sbt:/root/.sbt + - ${{ github.workspace }}/../../cache/ivy:/root/.ivy2/cache + - ${{ github.workspace }}/../../cache/general:/root/.cache + + if: "( + github.event_name == 'pull_request' + && !contains(github.event.pull_request.body, '[skip ci]') + && contains(github.event.pull_request.body, '[test_java18]') + )" + + steps: + - name: Install JDK 18 + run: curl https://download.java.net/java/GA/jdk18/43f95e8614114aeaa8e8a5fcf20a682d/36/GPL/openjdk-18_linux-x64_bin.tar.gz | tar -C /usr/lib/jvm -xzf - + + - name: Set JDK 18 as default + run: echo "/usr/lib/jvm/jdk-18/bin" >> $GITHUB_PATH + + - name: Reset existing repo + run: git -c "http.https://github.com/.extraheader=" fetch --recurse-submodules=no "https://github.com/lampepfl/dotty" && git reset --hard FETCH_HEAD || true + + - name: Checkout cleanup script + uses: actions/checkout@v2 + + - name: Cleanup + run: .github/workflows/cleanup.sh + + - name: Git Checkout + uses: actions/checkout@v2 + + - name: Add SBT proxy repositories + run: cp -vf .github/workflows/repositories /root/.sbt/ ; true + + - name: Test + run: | + ./project/scripts/sbt "scala3-bootstrapped/compile; scala3-bootstrapped/testOnly dotty.tools.repl.Disassemb* dotty.tools.repl.Javap* dotty.tools.repl.Asmp*" + + test_java19: + runs-on: [self-hosted, Linux] + container: + image: lampepfl/dotty:2021-03-22 + options: --cpu-shares 4096 + volumes: + - ${{ github.workspace }}/../../cache/sbt:/root/.sbt + - ${{ github.workspace }}/../../cache/ivy:/root/.ivy2/cache + - ${{ github.workspace }}/../../cache/general:/root/.cache + + if: "( + github.event_name == 'pull_request' + && !contains(github.event.pull_request.body, '[skip ci]') + && contains(github.event.pull_request.body, '[test_java19]') + )" + + steps: + - name: Install JDK 19 + run: curl https://download.java.net/java/early_access/jdk19/21/GPL/openjdk-19-ea+21_linux-x64_bin.tar.gz | tar -C /usr/lib/jvm -xzf - + + - name: Set JDK 19 as default + run: echo "/usr/lib/jvm/jdk-19/bin" >> $GITHUB_PATH + + - name: Reset existing repo + run: git -c "http.https://github.com/.extraheader=" fetch --recurse-submodules=no "https://github.com/lampepfl/dotty" && git reset --hard FETCH_HEAD || true + + - name: Checkout cleanup script + uses: actions/checkout@v2 + + - name: Cleanup + run: .github/workflows/cleanup.sh + + - name: Git Checkout + uses: actions/checkout@v2 + + - name: Add SBT proxy repositories + run: cp -vf .github/workflows/repositories /root/.sbt/ ; true + + - name: Test + run: | + ./project/scripts/sbt "scala3-bootstrapped/compile; scala3-bootstrapped/testOnly dotty.tools.repl.Disassemb* dotty.tools.repl.Javap* dotty.tools.repl.Asmp*" + publish_nightly: runs-on: [self-hosted, Linux] container: diff --git a/compiler/src/dotty/tools/dotc/profile/Profiler.scala b/compiler/src/dotty/tools/dotc/profile/Profiler.scala index ae86713a378c..27d09d64228e 100644 --- a/compiler/src/dotty/tools/dotc/profile/Profiler.scala +++ b/compiler/src/dotty/tools/dotc/profile/Profiler.scala @@ -123,6 +123,7 @@ private [profile] class RealProfiler(reporter : ProfileReporter)(using Context) } private def readHeapUsage() = RealProfiler.memoryMx.getHeapMemoryUsage.getUsed + @annotation.nowarn("cat=deprecation") private def doGC: Unit = { System.gc() System.runFinalization() diff --git a/library/src/scala/runtime/LazyVals.scala b/library/src/scala/runtime/LazyVals.scala index 85ca4f5cb3a0..67d5298ceefe 100644 --- a/library/src/scala/runtime/LazyVals.scala +++ b/library/src/scala/runtime/LazyVals.scala @@ -100,6 +100,7 @@ object LazyVals { } def getOffset(clz: Class[_], name: String): Long = { + @annotation.nowarn("cat=deprecation") val r = unsafe.objectFieldOffset(clz.getDeclaredField(name)) if (debug) println(s"getOffset($clz, $name) = $r")