/* BSD 2-Clause License - see OPAL/LICENSE for details. */
package org.opalj
package ai
package common
import scala.util.boundary.Break
import scala.xml.Node
import scala.xml.NodeSeq
import scala.xml.Text
import scala.xml.Unparsed
import org.opalj.br.*
import org.opalj.br.instructions.*
import org.opalj.collection.immutable.IntTrieSet
import org.opalj.io.writeAndOpen
/**
* 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 val _lastDump = new java.util.concurrent.atomic.AtomicLong(0L)
private def lastDump_=(currentTimeMillis: Long): Unit = {
_lastDump.set(currentTimeMillis)
}
private 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 b: Break[?] => throw b
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 b: Break[?] => throw b
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
)(using 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}
}
}
}