/* BSD 2-Clause License - see OPAL/LICENSE for details. */
package org.opalj
package ai
package common
import scala.util.control.ControlThrowable
import scala.xml.Node
import scala.xml.NodeSeq
import scala.xml.Unparsed
import scala.xml.Text
import org.opalj.io.writeAndOpen
import org.opalj.collection.immutable.IntTrieSet
import org.opalj.br._
import org.opalj.br.instructions._
/**
* Several utility methods to facilitate the development of the abstract interpreter/
* new domains for the abstract interpreter, by creating various kinds of dumps of
* the state of the interpreter.
*
* ==Thread Safety==
* This object is thread-safe.
*
* @author Michael Eichberg
*/
object XHTML {
import org.opalj.ai.util.XHTML._
/**
* Stores the time when the last dump was created.
*
* We generate dumps on errors only if the specified time has passed by to avoid that
* we are drowned in dumps. Often, a single bug causes many dumps to be created.
*/
private[this] val _lastDump = new java.util.concurrent.atomic.AtomicLong(0L)
private[this] def lastDump_=(currentTimeMillis: Long): Unit = {
_lastDump.set(currentTimeMillis)
}
private[this] def lastDump = _lastDump.get()
def dumpOnFailure[T, D <: Domain](
classFile: ClassFile,
method: Method,
ai: AI[_ >: D],
theDomain: D,
minimumDumpInterval: Long = 500L
)(
f: AIResult { val domain: theDomain.type } => T
): T = {
val result = ai(method, theDomain)
val operandsArray = result.operandsArray
val localsArray = result.localsArray
try {
if (result.wasAborted) throw new RuntimeException("interpretation aborted")
f(result)
} catch {
case ct: ControlThrowable => throw ct
case e: Throwable =>
val currentTime = System.currentTimeMillis()
if ((currentTime - this.lastDump) > minimumDumpInterval) {
this.lastDump = currentTime
val title = Some("Generated due to exception: "+e.getMessage())
val code = method.body.get
val dump =
XHTML.dump(
Some(classFile), Some(method), code, title, theDomain
)(result.cfJoins, operandsArray, localsArray)
writeAndOpen(dump, "StateOfIncompleteAbstractInterpretation", ".html")
} else {
Console.err.println("[info] dump suppressed: "+e.getMessage())
}
throw e
}
}
/**
* In case that during the validation some exception is thrown, a dump of
* the current memory layout is written to a temporary file and opened in a
* browser; the number of dumps that are generated is controlled by
* `timeInMillisBetweenDumps`. If a dump is suppressed a short message is
* printed on the console.
*
* @param f The function that performs the validation of the results.
*/
def dumpOnFailureDuringValidation[T](
classFile: Option[ClassFile],
method: Option[Method],
code: Code,
result: AIResult,
minimumDumpInterval: Long = 500L
)(
f: => T
): T = {
try {
if (result.wasAborted) throw new RuntimeException("interpretation aborted")
f
} catch {
case ct: ControlThrowable => throw ct
case e: Throwable =>
val currentTime = System.currentTimeMillis()
if ((currentTime - this.lastDump) > minimumDumpInterval) {
this.lastDump = currentTime
val message = "Dump generated due to exception: "+e.getMessage
writeAndOpen(
dump(classFile.get, method.get, message, result),
"AIResult",
".html"
)
} else {
Console.err.println("dump suppressed: "+e.getMessage)
}
throw e
}
}
def dump(
classFile: ClassFile,
method: Method,
resultHeader: String,
result: AIResult
): Node = {
import result._
val title = method.toJava
createXHTML(
Some(title),
Seq[Node](
{ title }
,
annotationsAsXHTML(method),
scala.xml.Unparsed(resultHeader),
dumpAIState(code, domain)(cfJoins, operandsArray, localsArray)
)
)
}
def annotationsAsXHTML(method: Method): Node =
{
this.annotations(method) map { annotation =>
val info = annotation.replace("\n", "
").replace("\t", " ")
{ Unparsed(info) }
}
}
def dump(
classFile: Option[ClassFile],
method: Option[Method],
code: Code,
resultHeader: Option[String],
domain: Domain
)(
cfJoins: IntTrieSet,
operandsArray: TheOperandsArray[domain.Operands],
localsArray: TheLocalsArray[domain.Locals]
): Node = {
def methodToString(method: Method): String = method.signatureToJava(withVisibility = false)
val title =
classFile.
map(_.thisType.toJava + method.map("{ "+methodToString(_)+" }").getOrElse("")).
orElse(method.map(methodToString))
val annotations = method.map(annotationsAsXHTML).getOrElse()
createXHTML(
title,
Seq[Node](
title.map(t => { t }
).getOrElse(Text("")),
annotations,
scala.xml.Unparsed(resultHeader.getOrElse("")),
dumpAIState(code, domain)(cfJoins, operandsArray, localsArray)
)
)
}
def dumpAIState(
code: Code,
domain: Domain
)(
cfJoins: IntTrieSet,
operandsArray: TheOperandsArray[domain.Operands],
localsArray: TheLocalsArray[domain.Locals]
): Node = {
val indexedExceptionHandlers = indexExceptionHandlers(code).toSeq.sortWith(_._2 < _._2)
val exceptionHandlers =
(
for ((eh, index) <- indexedExceptionHandlers) yield {
"⚡: "+index+" "+eh.catchType.map(_.toJava).getOrElse("")+
" ["+eh.startPC+","+eh.endPC+")"+" => "+eh.handlerPC
}
).map(eh => { eh }
)
val ids = new java.util.IdentityHashMap[AnyRef, Integer]
var nextId = 1
val idsLookup = (value: AnyRef) => {
var id = ids.get(value)
if (id == null) {
id = nextId
nextId += 1
ids.put(value, id)
}
id.intValue()
}
// We cannot create a "reasonable output in case of VERY VERY large methods"
// E.g., a method with 30000 instructions and 1000 locals would create
// a table with ~ 30.000.000 rows...
val rowsCount = code.instructionsCount * code.maxLocals
val operandsOnly = rowsCount > 100000
val disclaimer =
if (operandsOnly) {
Output is restricted to the operands as the number of rows would be too large otherwise: { rowsCount }
} else
NodeSeq.Empty
{ disclaimer }
PC |
Instruction |
Operand Stack |
{
if (operandsOnly) NodeSeq.Empty else {
Registers |
Properties |
}
}
{
dumpInstructions(
code, domain, operandsOnly
)(
cfJoins,
operandsArray, localsArray
)(Some(idsLookup))
}
{ exceptionHandlers }
}
private def annotations(method: Method): Seq[String] = {
val annotations = method.runtimeVisibleAnnotations ++ method.runtimeInvisibleAnnotations
annotations.map(_.toJava)
}
private def indexExceptionHandlers(code: Code) = code.exceptionHandlers.zipWithIndex.toMap
private def dumpInstructions(
code: Code,
domain: Domain,
operandsOnly: Boolean
)(
cfJoins: IntTrieSet,
operandsArray: TheOperandsArray[domain.Operands],
localsArray: TheLocalsArray[domain.Locals]
)(
implicit
ids: Option[AnyRef => Int]
): Array[Node] = {
val belongsToSubroutine = code.belongsToSubroutine()
val indexedExceptionHandlers = indexExceptionHandlers(code)
val instrs = code.instructions.zipWithIndex.zip(operandsArray zip localsArray).filter(_._1._1 ne null)
for (((instruction, pc), (operands, locals)) <- instrs) yield {
var exceptionHandlers = code.handlersFor(pc).map(indexedExceptionHandlers(_)).mkString(",")
if (exceptionHandlers.nonEmpty) exceptionHandlers = "⚡: "+exceptionHandlers
dumpInstruction(
pc, code.lineNumber(pc), instruction, cfJoins.contains(pc),
belongsToSubroutine(pc),
Some(exceptionHandlers),
domain,
operandsOnly
)(operands, locals)
}
}
def dumpInstruction(
pc: Int,
lineNumber: Option[Int],
instruction: Instruction,
pathsJoin: Boolean,
subroutineId: Int,
exceptionHandlers: Option[String],
domain: Domain,
operandsOnly: Boolean
)(
operands: domain.Operands,
locals: domain.Locals
)(
implicit
ids: Option[AnyRef => Int]
): Node = {
val pcAsXHTML =
Unparsed(
(if (pathsJoin) "⇉ " else "") + pc.toString +
exceptionHandlers.map("
"+_).getOrElse("") +
lineNumber.map("
l="+_+"").getOrElse("") +
(if (subroutineId != 0) "
⥂="+subroutineId+"" else "")
)
val properties = htmlify(domain.properties(pc, valueToString).getOrElse(""))
val instructionAsXHTML =
// to handle cases where the string contains "executable" (JavaScript) code
Unparsed(Text(instruction.toString(pc)).toString.replace("\n", "
"))
{ pcAsXHTML } |
{ instructionAsXHTML } |
{ dumpStack(operands) } |
{
if (operandsOnly) NodeSeq.Empty else {
{ dumpLocals(locals) } |
{ properties } |
}
}
}
def dumpStack(operands: Operands[_ <: AnyRef])(implicit ids: Option[AnyRef => Int]): Node = {
if (operands eq null)
Information about operands is not available.
else {
{ operands.map(op => - { valueToString(op) }
) }
}
}
def dumpLocals(locals: Locals[_ <: AnyRef])(implicit ids: Option[AnyRef => Int]): Node = {
def mapLocal(local: AnyRef): Node = {
if (local eq null)
{ "UNUSED" }
else
{ valueToString(local) }
}
if (locals eq null)
Information about the local variables is not available.
else {
{ locals.map { mapLocal }.iterator }
}
}
}