Skip to content

Commit eb86d96

Browse files
committed
Add :asmp to the REPL
Provides bytecode disassembly using the ASM library bundled with the Scala compiler.
1 parent 0022c47 commit eb86d96

File tree

4 files changed

+266
-0
lines changed

4 files changed

+266
-0
lines changed

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

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

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,12 @@ class ReplDriver(settings: Array[String],
393393
state
394394
}
395395

396+
case AsmpOf(line) =>
397+
given DisassemblerRepl(this, state)
398+
val opts = AsmpOptions.parse(ReplStrings.words(line))
399+
disassemble(Asmp, opts)
400+
state
401+
396402
case JavapOf(line) =>
397403
given DisassemblerRepl(this, state)
398404
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 { implicit state =>
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 { implicit state =>
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 { implicit state =>
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+
fromInitialState { implicit state =>
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

0 commit comments

Comments
 (0)