Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 53568e1

Browse files
committedJun 24, 2021
Add :asmp to the REPL
1 parent 680be40 commit 53568e1

File tree

4 files changed

+240
-0
lines changed

4 files changed

+240
-0
lines changed
 

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

+177
Original file line numberDiff line numberDiff line change
@@ -575,3 +575,180 @@ class JavapTask(val loader: ClassLoader, repl: DisassemblerRepl) extends Disasse
575575
inputs.map(runInput).toList
576576
end apply
577577
end JavapTask
578+
579+
object Asmp extends Disassembler:
580+
import Disassembler.*
581+
582+
def apply(opts: DisassemblerOptions)(using repl: DisassemblerRepl): List[DisResult] =
583+
val tool = AsmpTool()
584+
val clazz = DisassemblyClass(repl.classLoader, repl)
585+
tool(opts.flags)(opts.targets.map(clazz.bytes(_)))
586+
587+
val helps = List(
588+
"usage" -> ":asmp [opts] [path or class or -]...",
589+
"-help" -> "Prints this help message",
590+
"-verbose/-v" -> "Stack size, number of locals, method args",
591+
"-private/-p" -> "Private classes and members",
592+
"-package" -> "Package-private classes and members",
593+
"-protected" -> "Protected classes and members",
594+
"-public" -> "Public classes and members",
595+
"-c" -> "Disassembled code",
596+
"-s" -> "Internal type signatures",
597+
"-filter" -> "Filter REPL machinery from output",
598+
"-raw" -> "Don't post-process output from ASM", // TODO for debugging
599+
"-decls" -> "Declarations",
600+
"-bridges" -> "Bridges",
601+
"-synthetics" -> "Synthetics",
602+
)
603+
604+
override def filters(target: String, opts: DisassemblerOptions): List[String => String] =
605+
val commonFilters = super.filters(target, opts)
606+
if opts.flags.contains("-decls") then filterCommentsBlankLines :: commonFilters
607+
else squashConsectiveBlankLines :: commonFilters
608+
609+
private def squashConsectiveBlankLines(s: String) = s.replaceAll("\n{3,}", "\n\n")
610+
611+
private def filterCommentsBlankLines(s: String): String =
612+
val comment = raw"\s*// .*".r
613+
def isBlankLine(s: String) = s.trim == ""
614+
def isComment(s: String) = comment.matches(s)
615+
filteredLines(s, t => !isComment(t) && !isBlankLine(t))
616+
end Asmp
617+
618+
object AsmpOptions extends DisassemblerOptionParser(Asmp.helps):
619+
val defaultToolOptions = List("-protected", "-verbose")
620+
621+
class AsmpTool extends DisassemblyTool:
622+
import DisassemblyTool.*
623+
import Disassembler.splitHashMember
624+
import java.io.{PrintWriter, StringWriter}
625+
import scala.tools.asm.{Attribute, ClassReader, Label, Opcodes}
626+
import scala.tools.asm.util.{Textifier, TraceClassVisitor}
627+
import dotty.tools.backend.jvm.ClassNode1
628+
629+
enum Mode:
630+
case Verbose, Code, Signatures
631+
632+
class FilteringTextifier(opts: Seq[String], mode: Mode, accessFilter: Int => Boolean, nameFilter: Option[String]) extends Textifier(Opcodes.ASM9):
633+
private def keep(access: Int, name: String): Boolean =
634+
accessFilter(access) && nameFilter.map(_ == name).getOrElse(true)
635+
636+
override def visitField(access: Int, name: String, descriptor: String, signature: String, value: Any): Textifier =
637+
if keep(access, name) then
638+
super.visitField(access, name, descriptor, signature, value)
639+
addNewTextifier(discard = (mode == Mode.Signatures))
640+
else
641+
addNewTextifier(discard = true)
642+
643+
override def visitMethod(access:Int, name: String, descriptor: String, signature: String, exceptions: Array[String]): Textifier =
644+
if keep(access, name) then
645+
super.visitMethod(access, name, descriptor, signature, exceptions)
646+
addNewTextifier(discard = (mode == Mode.Signatures))
647+
else
648+
addNewTextifier(discard = true)
649+
650+
override def visitInnerClass(name: String, outerName: String, innerName: String, access: Int): Unit =
651+
if mode == Mode.Verbose && keep(access, name) then
652+
super.visitInnerClass(name, outerName, innerName, access)
653+
654+
override def visitClassAttribute(attribute: Attribute): Unit =
655+
if mode == Mode.Verbose && nameFilter.isEmpty then
656+
super.visitClassAttribute(attribute)
657+
658+
override def visitClassAnnotation(descriptor: String, visible: Boolean): Textifier =
659+
// suppress ScalaSignature unless -raw given. Should we? TODO
660+
if mode == Mode.Verbose && nameFilter.isEmpty && descriptor != "Lscala/reflect/ScalaSignature;" then
661+
super.visitClassAnnotation(descriptor, visible)
662+
else
663+
addNewTextifier(discard = true)
664+
665+
override def visitSource(file: String, debug: String): Unit =
666+
if mode == Mode.Verbose && nameFilter.isEmpty then
667+
super.visitSource(file, debug)
668+
669+
override def visitAnnotation(descriptor: String, visible: Boolean): Textifier =
670+
if mode == Mode.Verbose then
671+
super.visitAnnotation(descriptor, visible)
672+
else
673+
addNewTextifier(discard = true)
674+
675+
override def visitLineNumber(line: Int, start: Label): Unit =
676+
if mode == Mode.Verbose then
677+
super.visitLineNumber(line, start)
678+
679+
override def visitMaxs(maxStack: Int, maxLocals: Int): Unit =
680+
if mode == Mode.Verbose then
681+
super.visitMaxs(maxStack, maxLocals)
682+
683+
override def visitLocalVariable(name: String, descriptor: String, signature: String, start: Label, end: Label, index: Int): Unit =
684+
if mode == Mode.Verbose then
685+
super.visitLocalVariable(name, descriptor, signature, start, end, index)
686+
687+
private def isLabel(s: String) = raw"\s*L\d+\s*".r.matches(s)
688+
689+
// ugly hack to prevent orphaned label when local vars, max stack not displayed (e.g. in -c mode)
690+
override def visitMethodEnd(): Unit = text.size match
691+
case 0 =>
692+
case n =>
693+
if isLabel(text.get(n - 1).toString) then
694+
try text.remove(n - 1)
695+
catch case _: UnsupportedOperationException => ()
696+
697+
private def addNewTextifier(discard: Boolean = false): Textifier =
698+
val tx = FilteringTextifier(opts, mode, accessFilter, nameFilter)
699+
if !discard then text.add(tx.getText());
700+
tx
701+
end FilteringTextifier
702+
703+
override def apply(options: Seq[String])(inputs: Seq[Input]): List[DisResult] =
704+
def parseAccessOption(opts: Seq[String]): Int =
705+
if opts.contains("-public") then Opcodes.ACC_PUBLIC
706+
else if opts.contains("-protected") then Opcodes.ACC_PROTECTED
707+
else if opts.contains("-private") || opts.contains("-p") then Opcodes.ACC_PRIVATE
708+
else 0
709+
710+
def accessFilter(opts: Seq[String]): Int => Boolean =
711+
inline def contains(mask: Int) = (a: Int) => (a & mask) != 0
712+
inline def excludes(mask: Int) = (a: Int) => (a & mask) == 0
713+
def accessible: Int => Boolean = parseAccessOption(opts) match
714+
case Opcodes.ACC_PUBLIC => contains(Opcodes.ACC_PUBLIC)
715+
case Opcodes.ACC_PROTECTED => contains(Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED)
716+
case Opcodes.ACC_PRIVATE => _ => true
717+
case _ /*package*/ => excludes(Opcodes.ACC_PRIVATE)
718+
def included(access: Int): Boolean = mode(opts) match
719+
case Mode.Verbose => true
720+
case _ =>
721+
val isBridge = contains(Opcodes.ACC_BRIDGE)(access)
722+
val isSynthetic = contains(Opcodes.ACC_SYNTHETIC)(access)
723+
if isSynthetic && opts.contains("-synthetics") then true
724+
else if isBridge && opts.contains("-bridges") then true
725+
else if isSynthetic || isBridge then false
726+
else true
727+
(x: Int) => accessible(x) && included(x)
728+
729+
def mode(opts: Seq[String]): Mode =
730+
if opts.contains("-c") then Mode.Code
731+
else if opts.contains("-s") || opts.contains("-decls") then Mode.Signatures
732+
else Mode.Verbose // default
733+
734+
def runInput(input: Input): DisResult = input match
735+
case Input(target, actual, Success(bytes)) =>
736+
val sw = StringWriter()
737+
val pw = PrintWriter(sw)
738+
val node = ClassNode1()
739+
740+
def nameFilter = splitHashMember(target).map(s => if s.isEmpty then "apply" else s)
741+
val tx =
742+
if options.contains("-raw") then Textifier()
743+
else FilteringTextifier(options, mode(options), accessFilter(options), nameFilter)
744+
745+
ClassReader(bytes).accept(node, 0)
746+
node.accept(TraceClassVisitor(null, tx, pw))
747+
pw.flush()
748+
DisSuccess(target, sw.toString)
749+
case Input(_, _, Failure(e)) =>
750+
DisError(e.getMessage)
751+
end runInput
752+
753+
inputs.map(runInput).toList
754+
end AsmpTool

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

+7
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ object Load {
5252
val command: String = ":load"
5353
}
5454

55+
case class AsmpOf(args: String) extends Command
56+
object AsmpOf {
57+
val command: String = ":asmp"
58+
}
59+
5560
case class JavapOf(args: String) extends Command
5661
object JavapOf {
5762
val command: String = ":javap"
@@ -106,6 +111,7 @@ case object Help extends Command {
106111
|
107112
|:help print this summary
108113
|:load <path> interpret lines in a file
114+
|:asmp <path|class> disassemble a file or class name
109115
|:javap <path|class> disassemble a file or class name
110116
|:quit exit the interpreter
111117
|:type <expression> evaluate the type of the given expression
@@ -135,6 +141,7 @@ object ParseResult {
135141
Load.command -> (arg => Load(arg)),
136142
TypeOf.command -> (arg => TypeOf(arg)),
137143
DocOf.command -> (arg => DocOf(arg)),
144+
AsmpOf.command -> (arg => AsmpOf(arg)),
138145
JavapOf.command -> (arg => JavapOf(arg))
139146
)
140147

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

+6
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,12 @@ class ReplDriver(settings: Array[String],
394394
state
395395
}
396396

397+
case AsmpOf(line) =>
398+
given DisassemblerRepl = DisassemblerRepl(this, state)
399+
val opts = AsmpOptions.parse(ReplStrings.words(line))
400+
disassemble(Asmp, opts)
401+
state
402+
397403
case JavapOf(line) =>
398404
given DisassemblerRepl = DisassemblerRepl(this, state)
399405
val opts = JavapOptions.parse(ReplStrings.words(line))

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

+50
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,56 @@ class JavapTests extends DisassemblerTest:
235235
}
236236
end JavapTests
237237

238+
class AsmpTests extends DisassemblerTest:
239+
val packageSeparator = "/"
240+
241+
@Test def `simple end-to-end` =
242+
eval("class Foo1").andThen { implicit state =>
243+
run(":asmp -c Foo1")
244+
assertDisassemblyIncludes(List(
245+
s"public class ${line(1, "Foo1")} {",
246+
"public <init>()V",
247+
"INVOKESPECIAL java/lang/Object.<init> ()V",
248+
))
249+
}
250+
251+
@Test def `multiple classes in prev entry` =
252+
eval {
253+
"""class Foo2
254+
|trait Bar2
255+
|""".stripMargin
256+
} andThen { implicit state =>
257+
run(":asmp -c -")
258+
assertDisassemblyIncludes(List(
259+
s"public class ${line(1, "Foo2")} {",
260+
s"public abstract interface ${line(1, "Bar2")} {",
261+
))
262+
}
263+
264+
@Test def `private selected method` =
265+
eval {
266+
"""class Baz1:
267+
| private def one = 1
268+
| private def two = 2
269+
|""".stripMargin
270+
} andThen { implicit state =>
271+
run(":asmp -p -c Baz1#one")
272+
val out = storedOutput()
273+
assertDisassemblyIncludes("private one()I", out)
274+
assertDisassemblyExcludes("private two()I", out)
275+
}
276+
277+
@Test def `java.lang.String signatures` =
278+
fromInitialState { implicit state =>
279+
run(":asmp -s java.lang.String")
280+
val out = storedOutput()
281+
assertDisassemblyIncludes("public static varargs format(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;", out)
282+
assertDisassemblyIncludes("public static join(Ljava/lang/CharSequence;Ljava/lang/Iterable;)Ljava/lang/String;", out)
283+
assertDisassemblyIncludes("public concat(Ljava/lang/String;)Ljava/lang/String;", out)
284+
assertDisassemblyIncludes("public trim()Ljava/lang/String;", out)
285+
}
286+
end AsmpTests
287+
238288
// Test option parsing
239289
class JavapOptionTests extends ReplTest:
240290
private def assertFlags(expected: Seq[String], input: Seq[String])(implicit s: State): Unit =

0 commit comments

Comments
 (0)
Please sign in to comment.