All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jacodb.analysis.impl.custom.FlowAnalysisImpl.kt Maven / Gradle / Ivy

/*
 *  Copyright 2022 UnitTestBot contributors (utbot.org)
 * 

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at *

* http://www.apache.org/licenses/LICENSE-2.0 *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jacodb.analysis.impl.custom import org.jacodb.api.cfg.JcBytecodeGraph import org.jacodb.api.cfg.JcGotoInst import java.util.* enum class Flow { IN { override fun getFlow(e: FlowEntry): F? { return e.inFlow } }, OUT { override fun getFlow(e: FlowEntry): F? { return e.outFlow } }; abstract fun getFlow(e: FlowEntry): F? } /** * Creates a new `Entry` graph based on a `JcGraph`. This includes pseudo topological order, local * access for predecessors and successors, a graph entry-point, connected component marker. */ private fun JcBytecodeGraph.newScope( direction: FlowAnalysisDirection, entryFlow: T, isForward: Boolean ): List> { val size = toList().size val s = ArrayDeque>(size) val scope = ArrayList>(size) val visited = HashMap>((size + 1) * 4 / 3) // out of scope node val instructions: List? val actualEntries = direction.entries(this) if (actualEntries.isNotEmpty()) { // normal cases: there is at least // one return statement for a backward analysis // or one entry statement for a forward analysis instructions = actualEntries } else { // cases without any entry statement if (isForward) { // case of a forward flow analysis on // a method without any entry point throw RuntimeException("No entry point for method in forward analysis") } else { // case of backward analysis on // a method which potentially has // an infinite loop and no return statement instructions = ArrayList() val head = entries.first() // collect all 'goto' statements to catch the 'goto' from the infinite loop val visitedInst = HashSet() val list = arrayListOf(head) var temp: NODE while (list.isNotEmpty()) { temp = list.removeAt(0) visitedInst.add(temp) // only add 'goto' statements if (temp is JcGotoInst) { instructions.add(temp) } for (next in successors(temp)) { if (visitedInst.contains(next)) { continue } list.add(next) } } if (instructions.isEmpty()) { throw RuntimeException("Backward analysis on an empty entry set.") } } } val root = RootEntry() root.visitEntry(instructions, visited) root.inFlow = entryFlow root.outFlow = entryFlow val sv: Array?> = arrayOfNulls(size) val si = IntArray(size) var index = 0 var i = 0 var entry: FlowEntry = root while (true) { if (i < entry.outs.size) { val next = entry.outs[i++] // an unvisited child node if (next.number == Int.MIN_VALUE) { next.number = s.size s.add(next) next.visitEntry(direction.outOf(this, next.data), visited) // save old si[index] = i sv[index] = entry index++ i = 0 entry = next } } else { if (index == 0) { assert(scope.size <= size) scope.reverse() return scope } scope.add(entry) s.pop(entry) // restore old index-- entry = sv[index]!! i = si[index] } } } private fun FlowEntry.visitEntry( instructions: List, visited: MutableMap> ): Array> { val n = instructions.size return Array(n) { instructions[it].toEntry(this, visited) }.also { outs = it } } private fun NODE.toEntry( pred: FlowEntry?, visited: MutableMap> ): FlowEntry { // either we reach a new node or a merge node, the latter one is rare // so put and restore should be better that a lookup val newEntry = LeafEntry(this, pred) val oldEntry = visited.putIfAbsent(this, newEntry) ?: return newEntry // no restore required // adding self ref (real strongly connected with itself) if (oldEntry === pred) { oldEntry.isStronglyConnected = true } // merge nodes are rare, so this is ok val length = oldEntry.ins.size oldEntry.ins = Arrays.copyOf(oldEntry.ins, length + 1) oldEntry.ins[length] = pred return oldEntry } private fun Deque>.pop(entry: FlowEntry) { var min = entry.number for (e in entry.outs) { assert(e.number > Int.MIN_VALUE) min = min.coerceAtMost(e.number) } // not our SCC if (min != entry.number) { entry.number = min return } // we only want real SCCs (size > 1) var last = removeLast() last.number = Int.MAX_VALUE if (last === entry) { return } last.isStronglyConnected = true while (true) { last = removeLast() assert(last.number >= entry.number) last.isStronglyConnected = true last.number = Int.MAX_VALUE if (last === entry) { assert(last.ins.size >= 2) return } } } enum class FlowAnalysisDirection { BACKWARD { override fun entries(g: JcBytecodeGraph): List { return g.exits } override fun outOf(g: JcBytecodeGraph, s: NODE): List { return g.predecessors(s).toList() } }, FORWARD { override fun entries(g: JcBytecodeGraph): List { return g.entries } override fun outOf(g: JcBytecodeGraph, s: NODE): List { return g.successors(s).toList() } }; abstract fun entries(g: JcBytecodeGraph): List abstract fun outOf(g: JcBytecodeGraph, s: NODE): List } abstract class FlowEntry(pred: FlowEntry?) { abstract val data: NODE var number = Int.MIN_VALUE var isStronglyConnected = false var ins: Array> = pred?.let { arrayOf(pred) } ?: emptyArray() var outs: Array> = emptyArray() var inFlow: T? = null var outFlow: T? = null override fun toString(): String { return data.toString() } } class RootEntry : FlowEntry(null) { override val data: NODE get() = throw IllegalStateException() } class LeafEntry(override val data: NODE, pred: FlowEntry?) : FlowEntry(pred) abstract class FlowAnalysisImpl(graph: JcBytecodeGraph) : AbstractFlowAnalysis(graph) { protected abstract fun flowThrough(instIn: T?, ins: NODE, instOut: T) fun outs(s: NODE): T { return outs[s] ?: newFlow() } override fun ins(s: NODE): T { return ins[s] ?: newFlow() } private fun Iterable>.initFlow() { // If a node has only a single in-flow, the in-flow is always equal // to the out-flow if its predecessor, so we use the same object. // this saves memory and requires less object creation and copy calls. // Furthermore a node can be marked as `canSkip`, this allows us to use // the same "flow-set" for out-flow and in-flow. T merge node with within // a real scc cannot be omitted, as it could cause endless loops within // the fixpoint-iteration! for (node in this) { var omit = true val inFlow: T val outFlow: T if (node.ins.size > 1) { inFlow = newFlow() // no merge points in loops omit = !node.isStronglyConnected } else { assert(node.ins.size == 1) { "Missing head" } val flow = getFlow(node.ins.first(), node) assert(flow != null) { "Topological order is broken" } inFlow = flow!! } if (omit && node.data.canSkip) { // We could recalculate the graph itself but that is more expensive than // just falling through such nodes. outFlow = inFlow } else { outFlow = newFlow() } node.inFlow = inFlow node.outFlow = outFlow ins[node.data] = inFlow outs[node.data] = outFlow } } /** * If a flow node can be skipped return `true`, otherwise `false`. There is no guarantee a node will * be omitted. `canSkip` node does not influence the result of an analysis. * * If you are unsure, don't overwrite this method */ protected open val NODE.canSkip: Boolean get() { return false } protected open fun getFlow(from: NODE, mergeNode: NODE) = Flow.OUT private fun getFlow(o: FlowEntry, e: FlowEntry): T? { return if (o.inFlow === o.outFlow) { o.outFlow } else { getFlow(o.data, e.data).getFlow(o) } } private fun FlowEntry.meetFlows() { assert(ins.isNotEmpty()) if (ins.size > 1) { var copy = true for (o in ins) { val flow = getFlow(o, this) val inFlow = inFlow if (flow != null && inFlow != null) { if (copy) { copy = false copy(flow, inFlow) } else { mergeInto(data, inFlow, flow) } } } } } open fun runAnalysis( direction: FlowAnalysisDirection, inFlow: Map, outFlow: Map ): Int { val scope = graph.newScope(direction, newEntryFlow(), isForward).also { it.initFlow() } val queue = PriorityQueue> { o1, o2 -> o1.number.compareTo(o2.number) } .also { it.addAll(scope) } // Perform fixed point flow analysis var numComputations = 0 while (true) { val entry = queue.poll() ?: return numComputations entry.meetFlows() val hasChanged = flowThrough(entry) // Update queue appropriately if (hasChanged) { queue.addAll(entry.outs.toList()) } numComputations++ } } private fun flowThrough(entry: FlowEntry): Boolean { if (entry.inFlow === entry.outFlow) { assert(!entry.isStronglyConnected || entry.ins.size == 1) return true } if (entry.isStronglyConnected) { // A flow node that is influenced by at least one back-reference. // It's essential to check if "flowThrough" changes the result. // This requires the calculation of "equals", which itself // can be really expensive - depending on the used flow-model. // Depending on the "merge"+"flowThrough" costs, it can be cheaper // to fall through. Only nodes with real back-references always // need to be checked for changes val out = newFlow() flowThrough(entry.inFlow, entry.data, out) if (out == entry.outFlow) { return false } // copy back the result, as it has changed entry.outFlow?.let { copy(out, it) } return true } // no back-references, just calculate "flowThrough" val outFlow = entry.outFlow if (outFlow != null) { flowThrough(entry.inFlow, entry.data, outFlow) } return true } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy