Skip to content

Commit c9bf3ab

Browse files
committed
Add :asmp to the REPL
Provides bytecode disassembly using the ASM library bundled with the Scala compiler.
1 parent 3eefff2 commit c9bf3ab

File tree

5 files changed

+267
-0
lines changed

5 files changed

+267
-0
lines changed

compiler/src/dotty/tools/repl/Disassembler.scala

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,3 +627,206 @@ object JavapTask:
627627
// introduced in JDK7 as internal API
628628
val taskClassName = "com.sun.tools.javap.JavapTask"
629629
end JavapTask
630+
631+
/** A disassembler implemented using the ASM library (a dependency of the backend)
632+
* Supports flags similar to javap, with some additions and omissions.
633+
*/
634+
object Asmp extends Disassembler:
635+
import Disassembler.*
636+
637+
def apply(opts: DisassemblerOptions)(using repl: DisassemblerRepl): List[DisResult] =
638+
val tool = AsmpTool()
639+
val clazz = DisassemblyClass(repl.classLoader)
640+
tool(opts.flags)(opts.targets.map(clazz.bytes(_)))
641+
642+
// The flags are intended to resemble those used by javap
643+
val helps = List(
644+
"usage" -> ":asmp [opts] [path or class or -]...",
645+
"-help" -> "Prints this help message",
646+
"-verbose/-v" -> "Stack size, number of locals, method args",
647+
"-private/-p" -> "Private classes and members",
648+
"-package" -> "Package-private classes and members",
649+
"-protected" -> "Protected classes and members",
650+
"-public" -> "Public classes and members",
651+
"-c" -> "Disassembled code",
652+
"-s" -> "Internal type signatures",
653+
"-filter" -> "Filter REPL machinery from output",
654+
"-raw" -> "Don't post-process output from ASM", // TODO for debugging
655+
"-decls" -> "Declarations",
656+
"-bridges" -> "Bridges",
657+
"-synthetics" -> "Synthetics",
658+
)
659+
660+
override def filters(target: String, opts: DisassemblerOptions): List[String => String] =
661+
val commonFilters = super.filters(target, opts)
662+
if opts.flags.contains("-decls") then filterCommentsBlankLines :: commonFilters
663+
else squashConsectiveBlankLines :: commonFilters // default filters
664+
665+
// A filter to compress consecutive blank lines into a single blank line
666+
private def squashConsectiveBlankLines(s: String) = s.replaceAll("\n{3,}", "\n\n").nn
667+
668+
// A filter to remove all blank lines and lines beginning with "//"
669+
private def filterCommentsBlankLines(s: String): String =
670+
val comment = raw"\s*// .*".r
671+
def isBlankLine(s: String) = s.trim == ""
672+
def isComment(s: String) = comment.matches(s)
673+
filteredLines(s, t => !isComment(t) && !isBlankLine(t))
674+
end Asmp
675+
676+
object AsmpOptions extends DisassemblerOptionParser(Asmp.helps):
677+
val defaultToolOptions = List("-protected", "-verbose")
678+
679+
/** Implementation of the ASM-based disassembly tool. */
680+
class AsmpTool extends DisassemblyTool:
681+
import DisassemblyTool.*
682+
import Disassembler.splitHashMember
683+
import java.io.{PrintWriter, StringWriter}
684+
import scala.tools.asm.{Attribute, ClassReader, Label, Opcodes}
685+
import scala.tools.asm.util.{Textifier, TraceClassVisitor}
686+
import dotty.tools.backend.jvm.ClassNode1
687+
688+
enum Mode:
689+
case Verbose, Code, Signatures
690+
691+
/** A Textifier subclass to control the disassembly output based on flags.
692+
* The visitor methods overriden here conditionally suppress their output
693+
* based on the flags and targets supplied to the disassembly tool.
694+
*
695+
* The filtering performed falls into three categories:
696+
* - operating mode: -verbose, -c, -s, etc.
697+
* - access flags: -protected, -private, -public, etc.
698+
* - member name: e.g. a target given as Klass#method
699+
*
700+
* This is all bypassed if the `-raw` flag is given.
701+
*/
702+
class FilteringTextifier(mode: Mode, accessFilter: Int => Boolean, nameFilter: Option[String])
703+
extends Textifier(Opcodes.ASM9):
704+
private def keep(access: Int, name: String): Boolean =
705+
accessFilter(access) && nameFilter.map(_ == name).getOrElse(true)
706+
707+
override def visitField(access: Int, name: String, descriptor: String, signature: String, value: Any): Textifier =
708+
if keep(access, name) then
709+
super.visitField(access, name, descriptor, signature, value)
710+
addNewTextifier(discard = (mode == Mode.Signatures))
711+
else
712+
addNewTextifier(discard = true)
713+
714+
override def visitMethod(access:Int, name: String, descriptor: String, signature: String, exceptions: Array[String | Null]): Textifier =
715+
if keep(access, name) then
716+
super.visitMethod(access, name, descriptor, signature, exceptions)
717+
addNewTextifier(discard = (mode == Mode.Signatures))
718+
else
719+
addNewTextifier(discard = true)
720+
721+
override def visitInnerClass(name: String, outerName: String, innerName: String, access: Int): Unit =
722+
if mode == Mode.Verbose && keep(access, name) then
723+
super.visitInnerClass(name, outerName, innerName, access)
724+
725+
override def visitClassAttribute(attribute: Attribute): Unit =
726+
if mode == Mode.Verbose && nameFilter.isEmpty then
727+
super.visitClassAttribute(attribute)
728+
729+
override def visitClassAnnotation(descriptor: String, visible: Boolean): Textifier | Null =
730+
// suppress ScalaSignature unless -raw given. Should we? TODO
731+
if mode == Mode.Verbose && nameFilter.isEmpty && descriptor != "Lscala/reflect/ScalaSignature;" then
732+
super.visitClassAnnotation(descriptor, visible)
733+
else
734+
addNewTextifier(discard = true)
735+
736+
override def visitSource(file: String, debug: String): Unit =
737+
if mode == Mode.Verbose && nameFilter.isEmpty then
738+
super.visitSource(file, debug)
739+
740+
override def visitAnnotation(descriptor: String, visible: Boolean): Textifier | Null =
741+
if mode == Mode.Verbose then
742+
super.visitAnnotation(descriptor, visible)
743+
else
744+
addNewTextifier(discard = true)
745+
746+
override def visitLineNumber(line: Int, start: Label): Unit =
747+
if mode == Mode.Verbose then
748+
super.visitLineNumber(line, start)
749+
750+
override def visitMaxs(maxStack: Int, maxLocals: Int): Unit =
751+
if mode == Mode.Verbose then
752+
super.visitMaxs(maxStack, maxLocals)
753+
754+
override def visitLocalVariable(name: String, descriptor: String, signature: String, start: Label, end: Label, index: Int): Unit =
755+
if mode == Mode.Verbose then
756+
super.visitLocalVariable(name, descriptor, signature, start, end, index)
757+
758+
private def isLabel(s: String) = raw"\s*L\d+\s*".r.matches(s)
759+
760+
// ugly hack to prevent orphaned label when local vars, max stack not displayed (e.g. in -c mode)
761+
override def visitMethodEnd(): Unit = if text != null then text.size match
762+
case 0 =>
763+
case n =>
764+
if isLabel(text.get(n - 1).toString) then
765+
try text.remove(n - 1)
766+
catch case _: UnsupportedOperationException => ()
767+
768+
private def addNewTextifier(discard: Boolean = false): Textifier =
769+
val tx = FilteringTextifier(mode, accessFilter, nameFilter)
770+
if !discard then text.nn.add(tx.getText())
771+
tx
772+
end FilteringTextifier
773+
774+
override def apply(options: Seq[String])(inputs: Seq[Input]): List[DisResult] =
775+
def parseMode(opts: Seq[String]): Mode =
776+
if opts.contains("-c") then Mode.Code
777+
else if opts.contains("-s") || opts.contains("-decls") then Mode.Signatures
778+
else Mode.Verbose // default
779+
780+
def parseAccessLevel(opts: Seq[String]): Int =
781+
if opts.contains("-public") then Opcodes.ACC_PUBLIC
782+
else if opts.contains("-protected") then Opcodes.ACC_PROTECTED
783+
else if opts.contains("-private") || opts.contains("-p") then Opcodes.ACC_PRIVATE
784+
else 0
785+
786+
def accessFilter(mode: Mode, accessLevel: Int, opts: Seq[String]): Int => Boolean =
787+
inline def contains(mask: Int) = (a: Int) => (a & mask) != 0
788+
inline def excludes(mask: Int) = (a: Int) => (a & mask) == 0
789+
val showSynthetics = opts.contains("-synthetics")
790+
val showBridges = opts.contains("-bridges")
791+
def accessible: Int => Boolean = accessLevel match
792+
case Opcodes.ACC_PUBLIC => contains(Opcodes.ACC_PUBLIC)
793+
case Opcodes.ACC_PROTECTED => contains(Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED)
794+
case Opcodes.ACC_PRIVATE => _ => true
795+
case _ /* package */ => excludes(Opcodes.ACC_PRIVATE)
796+
def included(access: Int): Boolean = mode match
797+
case Mode.Verbose => true
798+
case _ =>
799+
val isBridge = contains(Opcodes.ACC_BRIDGE)(access)
800+
val isSynthetic = contains(Opcodes.ACC_SYNTHETIC)(access)
801+
if isSynthetic && showSynthetics then true // TODO do we have tests for -synthetics?
802+
else if isBridge && showBridges then true // TODO do we have tests for -bridges?
803+
else if isSynthetic || isBridge then false
804+
else true
805+
a => accessible(a) && included(a)
806+
807+
def runInput(input: Input): DisResult = input match
808+
case Input(target, _, Success(bytes)) =>
809+
val sw = StringWriter()
810+
val pw = PrintWriter(sw)
811+
val node = ClassNode1()
812+
813+
val tx =
814+
if options.contains("-raw") then
815+
Textifier()
816+
else
817+
val mode = parseMode(options)
818+
val accessLevel = parseAccessLevel(options)
819+
val nameFilter = splitHashMember(target).map(s => if s.isEmpty then "apply" else s)
820+
FilteringTextifier(mode, accessFilter(mode, accessLevel, options), nameFilter)
821+
822+
ClassReader(bytes).accept(node, 0)
823+
node.accept(TraceClassVisitor(null, tx, pw))
824+
pw.flush()
825+
DisSuccess(target, sw.toString)
826+
case Input(_, _, Failure(e)) =>
827+
DisError(e.getMessage)
828+
end runInput
829+
830+
inputs.map(runInput).toList
831+
end apply
832+
end AsmpTool

compiler/src/dotty/tools/repl/ParseResult.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ object Load {
5252
val command: String = ":load"
5353
}
5454

55+
/** Run the ASM based disassembler on the given target(s) */
56+
case class AsmpOf(args: String) extends Command
57+
object AsmpOf:
58+
val command: String = ":asmp"
5559

5660
/** Run the javap disassembler on the given target(s) */
5761
case class JavapOf(args: String) extends Command
@@ -113,6 +117,7 @@ case object Help extends Command {
113117
|
114118
|:help print this summary
115119
|:load <path> interpret lines in a file
120+
|:asmp <path|class> disassemble a file or class name (experimental)
116121
|:javap <path|class> disassemble a file or class name
117122
|:quit exit the interpreter
118123
|:type <expression> evaluate the type of the given expression
@@ -144,6 +149,7 @@ object ParseResult {
144149
TypeOf.command -> (arg => TypeOf(arg)),
145150
DocOf.command -> (arg => DocOf(arg)),
146151
Settings.command -> (arg => Settings(arg)),
152+
AsmpOf.command -> (arg => AsmpOf(arg)),
147153
JavapOf.command -> (arg => JavapOf(arg))
148154
)
149155

compiler/src/dotty/tools/repl/ReplDriver.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,12 @@ class ReplDriver(settings: Array[String],
462462
state
463463
}
464464

465+
case AsmpOf(line) =>
466+
given DisassemblerRepl(this, state)
467+
val opts = AsmpOptions.parse(ReplStrings.words(line))
468+
disassemble(Asmp, opts)
469+
state
470+
465471
case JavapOf(line) =>
466472
given DisassemblerRepl(this, state)
467473
val opts = JavapOptions.parse(ReplStrings.words(line))

compiler/test/dotty/tools/repl/DisassemblerTests.scala

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,3 +488,54 @@ class JavapFilterSelectionTests:
488488
|""".stripMargin,
489489
Javap.filterSelection("List#sorted")(listC))
490490
end JavapFilterSelectionTests
491+
492+
// Test disassembly using `:asmp`
493+
class AsmpTests extends DisassemblerTest:
494+
override val packageSeparator = "/"
495+
496+
@Test def `simple end-to-end` =
497+
eval("class Foo1").andThen {
498+
run(":asmp -c Foo1")
499+
assertDisassemblyIncludes(List(
500+
s"public class ${line(1, "Foo1")} {",
501+
"public <init>()V",
502+
"INVOKESPECIAL java/lang/Object.<init> ()V",
503+
))
504+
}
505+
506+
@Test def `multiple classes in prev entry` =
507+
eval {
508+
"""class Foo2
509+
|trait Bar2
510+
|""".stripMargin
511+
} andThen {
512+
run(":asmp -c -")
513+
assertDisassemblyIncludes(List(
514+
s"public class ${line(1, "Foo2")} {",
515+
s"public abstract interface ${line(1, "Bar2")} {",
516+
))
517+
}
518+
519+
@Test def `private selected method` =
520+
eval {
521+
"""class Baz1:
522+
| private def one = 1
523+
| private def two = 2
524+
|""".stripMargin
525+
} andThen {
526+
run(":asmp -p -c Baz1#one")
527+
val out = storedOutput()
528+
assertDisassemblyIncludes("private one()I", out)
529+
assertDisassemblyExcludes("private two()I", out)
530+
}
531+
532+
@Test def `java.lang.String signatures` =
533+
initially {
534+
run(":asmp -s java.lang.String")
535+
val out = storedOutput()
536+
assertDisassemblyIncludes("public static varargs format(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;", out)
537+
assertDisassemblyIncludes("public static join(Ljava/lang/CharSequence;Ljava/lang/Iterable;)Ljava/lang/String;", out)
538+
assertDisassemblyIncludes("public concat(Ljava/lang/String;)Ljava/lang/String;", out)
539+
assertDisassemblyIncludes("public trim()Ljava/lang/String;", out)
540+
}
541+
end AsmpTests

compiler/test/dotty/tools/repl/TabcompleteTests.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ class TabcompleteTests extends ReplTest {
207207
@Test def commands = initially {
208208
assertEquals(
209209
List(
210+
":asmp",
210211
":doc",
211212
":exit",
212213
":help",

0 commit comments

Comments
 (0)