Skip to content

Commit 27a25a0

Browse files
authored
Proper iinc fix (#168)
JacoDB produces incorrect 3-address code for IINC instruction #146
1 parent 89d185c commit 27a25a0

File tree

7 files changed

+172
-94
lines changed

7 files changed

+172
-94
lines changed

jacodb-core/src/main/kotlin/org/jacodb/impl/cfg/RawInstListBuilder.kt

+29-32
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,6 @@ class RawInstListBuilder(
188188
private val laterAssignments = identityMap<AbstractInsnNode, MutableMap<Int, JcRawValue>>()
189189
private val laterStackAssignments = identityMap<AbstractInsnNode, MutableMap<Int, JcRawValue>>()
190190
private val localTypeRefinement = identityMap<JcRawLocalVar, JcRawLocalVar>()
191-
private val postfixInstructions = hashMapOf<Int, JcRawInst>()
192191

193192
private var labelCounter = 0
194193
private var localCounter = 0
@@ -213,12 +212,13 @@ class RawInstListBuilder(
213212
private fun buildInstructions() {
214213
currentFrame = createInitialFrame()
215214
frames[ENTRY] = currentFrame
216-
methodNode.instructions.forEachIndexed { index, insn ->
215+
val nodes = methodNode.instructions.toList()
216+
nodes.forEachIndexed { index, insn ->
217217
when (insn) {
218218
is InsnNode -> buildInsnNode(insn)
219219
is FieldInsnNode -> buildFieldInsnNode(insn)
220220
is FrameNode -> buildFrameNode(insn)
221-
is IincInsnNode -> buildIincInsnNode(insn)
221+
is IincInsnNode -> buildIincInsnNode(insn, nodes.getOrNull(index + 1))
222222
is IntInsnNode -> buildIntInsnNode(insn)
223223
is InvokeDynamicInsnNode -> buildInvokeDynamicInsn(insn)
224224
is JumpInsnNode -> buildJumpInsnNode(insn)
@@ -250,11 +250,14 @@ class RawInstListBuilder(
250250
val insnList = instructionList(insn)
251251
val frame = frames[insn]!!
252252
for ((variable, value) in assignments) {
253-
if (value != frame[variable]) {
254-
if (insn.isBranchingInst || insn.isTerminateInst) {
255-
insnList.addInst(insn, JcRawAssignInst(method, value, frame[variable]!!), insnList.lastIndex)
253+
val frameVariable = frame[variable]
254+
if (frameVariable != null && value != frameVariable) {
255+
if (insn.isBranchingInst) {
256+
insnList.addInst(insn, JcRawAssignInst(method, value, frameVariable), 0)
257+
}else if(insn.isTerminateInst) {
258+
insnList.addInst(insn, JcRawAssignInst(method, value, frameVariable), insnList.lastIndex)
256259
} else {
257-
insnList.addInst(insn, JcRawAssignInst(method, value, frame[variable]!!))
260+
insnList.addInst(insn, JcRawAssignInst(method, value, frameVariable))
258261
}
259262
}
260263
}
@@ -375,11 +378,16 @@ class RawInstListBuilder(
375378
return currentFrame.locals.getValue(variable)
376379
}
377380

378-
private fun local(variable: Int, expr: JcRawValue, insn: AbstractInsnNode): JcRawAssignInst? {
381+
private fun local(variable: Int, expr: JcRawValue, insn: AbstractInsnNode, override: Boolean = false): JcRawAssignInst? {
379382
val oldVar = currentFrame.locals[variable]
380383
return if (oldVar != null) {
381384
if (oldVar.typeName == expr.typeName || (expr is JcRawNullConstant && !oldVar.typeName.isPrimitive)) {
382-
JcRawAssignInst(method, oldVar, expr)
385+
if (override) {
386+
currentFrame = currentFrame.put(variable, expr)
387+
JcRawAssignInst(method, expr, expr)
388+
} else {
389+
JcRawAssignInst(method, oldVar, expr)
390+
}
383391
} else if (expr is JcRawSimpleValue) {
384392
currentFrame = currentFrame.put(variable, expr)
385393
null
@@ -403,7 +411,7 @@ class RawInstListBuilder(
403411
private fun instructionList(insn: AbstractInsnNode) = instructions.getOrPut(insn, ::mutableListOf)
404412

405413
private fun addInstruction(insn: AbstractInsnNode, inst: JcRawInst, index: Int? = null) {
406-
instructionList(insn).addInst(insn, inst, index)
414+
instructionList(insn).addInst(insn, inst, index)
407415
}
408416

409417
private fun MutableList<JcRawInst>.addInst(node: AbstractInsnNode, inst: JcRawInst, index: Int? = null) {
@@ -412,18 +420,6 @@ class RawInstListBuilder(
412420
} else {
413421
add(inst)
414422
}
415-
if (postfixInstructions.isNotEmpty()) {
416-
when {
417-
node.isBranchingInst -> postfixInstructions.forEach {
418-
instructionList(node).add(0, it.value)
419-
}
420-
421-
inst !is JcRawReturnInst -> postfixInstructions.forEach {
422-
instructionList(node).add(it.value)
423-
}
424-
}
425-
postfixInstructions.clear()
426-
}
427423
}
428424

429425
private fun nextRegister(typeName: TypeName): JcRawValue {
@@ -825,7 +821,7 @@ class RawInstListBuilder(
825821
* a helper function that helps to merge local variables from several predecessor frames into one map
826822
* if all the predecessor frames are known (meaning we already visited all the corresponding instructions
827823
* in the bytecode) --- merge process is trivial
828-
* if some predecessor frames are unknown, we remebmer them and add requried assignment instructions after
824+
* if some predecessor frames are unknown, we remember them and add required assignment instructions after
829825
* the full construction process is complete, see #buildRequiredAssignments function
830826
*/
831827
private fun SortedMap<Int, TypeName>.copyLocals(predFrames: Map<AbstractInsnNode, Frame?>): Map<Int, JcRawValue> =
@@ -1017,13 +1013,18 @@ class RawInstListBuilder(
10171013
}
10181014
}
10191015

1020-
private fun buildIincInsnNode(insnNode: IincInsnNode) {
1016+
private fun buildIincInsnNode(insnNode: IincInsnNode, nextInst: AbstractInsnNode?) {
10211017
val variable = insnNode.`var`
10221018
val local = local(variable)
1023-
postfixInstructions[variable] = JcRawAssignInst(method, local,
1024-
JcRawAddExpr(local.typeName, local, JcRawInt(insnNode.incr))
1025-
)
1026-
local(variable, local, insnNode)
1019+
val incrementedVariable = when {
1020+
nextInst != null && nextInst.isBranchingInst -> local
1021+
nextInst != null && (
1022+
(nextInst is VarInsnNode && nextInst.`var` == variable) || nextInst is LabelNode) -> local
1023+
else -> nextRegister(local.typeName)
1024+
}
1025+
val add = JcRawAddExpr(local.typeName, local, JcRawInt(insnNode.incr))
1026+
instructionList(insnNode) += JcRawAssignInst(method, incrementedVariable, add)
1027+
local(variable, incrementedVariable, insnNode, override = incrementedVariable != local)
10271028
}
10281029

10291030
private fun buildIntInsnNode(insnNode: IntInsnNode) {
@@ -1428,10 +1429,6 @@ class RawInstListBuilder(
14281429

14291430
in Opcodes.ILOAD..Opcodes.ALOAD -> {
14301431
push(local(variable))
1431-
postfixInstructions[variable]?.let {
1432-
postfixInstructions.remove(variable)
1433-
instructionList(insnNode).add(it) // do not reuse `addInstruction` function here
1434-
}
14351432
}
14361433
else -> error("Unknown opcode ${insnNode.opcode} in VarInsnNode")
14371434
}

jacodb-core/src/main/kotlin/org/jacodb/impl/cfg/Simplifier.kt

-1
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,6 @@ internal class Simplifier {
225225
}
226226
}
227227

228-
229228
private class SimplifierCollector : AbstractFullRawExprSetCollector() {
230229
val exprs = hashSetOf<JcRawSimpleValue>()
231230

jacodb-core/src/test/kotlin/org/jacodb/testing/cfg/BaseInstructionsTest.kt

+10-16
Original file line numberDiff line numberDiff line change
@@ -40,47 +40,41 @@ abstract class BaseInstructionsTest : BaseTest() {
4040

4141
val ext = runBlocking { cp.hierarchyExt() }
4242

43-
protected fun testClass(klass: JcClassOrInterface) {
44-
testAndLoadClass(klass, false)
43+
protected fun testClass(klass: JcClassOrInterface, validateLineNumbers: Boolean = true) {
44+
testAndLoadClass(klass, false, validateLineNumbers)
4545
}
4646

4747
protected fun testAndLoadClass(klass: JcClassOrInterface): Class<*> {
48-
return testAndLoadClass(klass, true)!!
48+
return testAndLoadClass(klass, true, validateLineNumbers = true)!!
4949
}
5050

51-
private fun testAndLoadClass(klass: JcClassOrInterface, loadClass: Boolean): Class<*>? {
51+
private fun testAndLoadClass(klass: JcClassOrInterface, loadClass: Boolean, validateLineNumbers: Boolean): Class<*>? {
5252
try {
5353
val classNode = klass.asmNode()
5454
classNode.methods = klass.declaredMethods.filter { it.enclosingClass == klass }.map {
5555
if (it.isAbstract || it.name.contains("$\$forInline")) {
5656
it.asmNode()
5757
} else {
5858
try {
59-
// val oldBody = it.body()
60-
// println()
61-
// println("Old body: ${oldBody.print()}")
6259
val instructionList = it.rawInstList
6360
it.instList.forEachIndexed { index, inst ->
6461
Assertions.assertEquals(index, inst.location.index, "indexes not matched for $it at $index")
6562
}
66-
// println("Instruction list: $instructionList")
6763
val graph = it.flowGraph()
6864
if (!it.enclosingClass.isKotlin) {
6965
val methodMsg = "$it should have line number"
70-
graph.instructions.forEach {
71-
Assertions.assertTrue(it.lineNumber > 0, methodMsg)
66+
if (validateLineNumbers) {
67+
graph.instructions.forEach {
68+
Assertions.assertTrue(it.lineNumber > 0, methodMsg)
69+
}
7270
}
7371
}
7472
graph.applyAndGet(OverridesResolver(ext)) {}
7573
JcGraphChecker(it, graph).check()
76-
// println("Graph: $graph")
77-
// graph.view("/usr/bin/dot", "/usr/bin/firefox", false)
78-
// graph.blockGraph().view("/usr/bin/dot", "/usr/bin/firefox")
7974
val newBody = MethodNodeBuilder(it, instructionList).build()
80-
// println("New body: ${newBody.print()}")
81-
// println()
8275
newBody
83-
} catch (e: Exception) {
76+
} catch (e: Throwable) {
77+
it.dumpInstructions()
8478
throw IllegalStateException("error handling $it", e)
8579
}
8680

jacodb-core/src/test/kotlin/org/jacodb/testing/cfg/IRTest.kt

+12-4
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,12 @@ class IRTest : BaseInstructionsTest() {
278278
testClass(cp.findClass<BinarySearchTree<*>.BinarySearchTreeIterator>())
279279
}
280280

281+
@Test
282+
fun `get ir of random class`() {
283+
val clazz = cp.findClass("kotlinx.coroutines.channels.ChannelsKt__DeprecatedKt\$filterIndexed\$1")
284+
val method = clazz.declaredMethods.first { it.name == "invokeSuspend" }
285+
JcGraphChecker(method, method.flowGraph()).check()
286+
}
281287

282288
@Test
283289
fun `get ir of self`() {
@@ -299,20 +305,22 @@ class IRTest : BaseInstructionsTest() {
299305
}
300306

301307
// todo: make this test green
302-
// @Test
308+
@Test
303309
fun `get ir of kotlinx-coroutines`() {
304310
// testClass(cp.findClass("kotlinx.coroutines.ThreadContextElementKt"))
305-
runAlongLib(kotlinxCoroutines)
311+
runAlongLib(kotlinxCoroutines, false)
306312
}
307313

314+
315+
308316
@AfterEach
309317
fun printStats() {
310318
cp.features!!.filterIsInstance<ClasspathCache>().forEach {
311319
it.dumpStats()
312320
}
313321
}
314322

315-
private fun runAlongLib(file: File) {
323+
private fun runAlongLib(file: File, validateLineNumbers: Boolean = true) {
316324
println("Run along: ${file.absolutePath}")
317325

318326
val classes = JarLocation(file, isRuntime = false, object : JavaVersion {
@@ -324,7 +332,7 @@ class IRTest : BaseInstructionsTest() {
324332
val clazz = cp.findClass(it.key)
325333
if (!clazz.isAnnotation && !clazz.isInterface) {
326334
println("Testing class: ${it.key}")
327-
testClass(clazz)
335+
testClass(clazz, validateLineNumbers)
328336
}
329337
}
330338
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright 2022 UnitTestBot contributors (utbot.org)
3+
* <p>
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.jacodb.testing.cfg
18+
19+
import org.jacodb.api.ext.findClass
20+
import org.jacodb.testing.WithDB
21+
import org.junit.jupiter.api.Assertions.assertArrayEquals
22+
import org.junit.jupiter.api.Assertions.assertEquals
23+
import org.junit.jupiter.api.Test
24+
25+
class IincTest : BaseInstructionsTest() {
26+
27+
companion object : WithDB()
28+
29+
@Test
30+
fun `iinc should work`() {
31+
val clazz = cp.findClass<Incrementation>()
32+
33+
val javaClazz = testAndLoadClass(clazz)
34+
val method = javaClazz.methods.first { it.name == "iinc" }
35+
val res = method.invoke(null, 0)
36+
assertEquals(0, res)
37+
}
38+
39+
@Test
40+
fun `iinc arrayIntIdx should work`() {
41+
val clazz = cp.findClass<Incrementation>()
42+
43+
val javaClazz = testAndLoadClass(clazz)
44+
val method = javaClazz.methods.first { it.name == "iincArrayIntIdx" }
45+
val res = method.invoke(null)
46+
assertArrayEquals(intArrayOf(1, 0, 2), res as IntArray)
47+
}
48+
49+
@Test
50+
fun `iinc arrayByteIdx should work`() {
51+
val clazz = cp.findClass<Incrementation>()
52+
53+
val javaClazz = testAndLoadClass(clazz)
54+
val method = javaClazz.methods.first { it.name == "iincArrayByteIdx" }
55+
val res = method.invoke(null)
56+
assertArrayEquals(intArrayOf(1, 0, 2), res as IntArray)
57+
}
58+
59+
@Test
60+
fun `iinc for`() {
61+
val clazz = cp.findClass<Incrementation>()
62+
63+
val javaClazz = testAndLoadClass(clazz)
64+
val method = javaClazz.methods.first { it.name == "iincFor" }
65+
val res = method.invoke(null)
66+
assertArrayEquals(intArrayOf(0, 1, 2, 3, 4), res as IntArray)
67+
}
68+
69+
@Test
70+
fun `iinc if`() {
71+
val clazz = cp.findClass<Incrementation>()
72+
73+
val javaClazz = testAndLoadClass(clazz)
74+
val method = javaClazz.methods.first { it.name == "iincIf" }
75+
assertArrayEquals(intArrayOf(), method.invoke(null, true, true) as IntArray)
76+
assertArrayEquals(intArrayOf(0), method.invoke(null, true, false) as IntArray)
77+
}
78+
79+
@Test
80+
fun `iinc if 2`() {
81+
val clazz = cp.findClass<Incrementation>()
82+
83+
val javaClazz = testAndLoadClass(clazz)
84+
val method = javaClazz.methods.first { it.name == "iincIf2" }
85+
assertEquals(2, method.invoke(null, 1))
86+
assertEquals(4, method.invoke(null, 2))
87+
}
88+
89+
// @Test
90+
fun `iinc while`() {
91+
val clazz = cp.findClass<Incrementation>()
92+
93+
val javaClazz = testAndLoadClass(clazz)
94+
val method = javaClazz.methods.first { it.name == "iincWhile" }
95+
assertEquals(2, method.invoke(null) as IntArray)
96+
}
97+
98+
}

jacodb-core/src/test/kotlin/org/jacodb/testing/cfg/InstructionsTest.kt

-41
Original file line numberDiff line numberDiff line change
@@ -267,47 +267,6 @@ class InstructionsTest : BaseInstructionsTest() {
267267
assertEquals("defaultMethod", callDefaultMethod.method.method.name)
268268
}
269269

270-
271-
@Test
272-
fun `iinc should work`() {
273-
val clazz = cp.findClass<Incrementation>()
274-
275-
val javaClazz = testAndLoadClass(clazz)
276-
val method = javaClazz.methods.first { it.name == "iinc" }
277-
val res = method.invoke(null, 0)
278-
assertEquals(0, res)
279-
}
280-
281-
@Test
282-
fun `iinc arrayIntIdx should work`() {
283-
val clazz = cp.findClass<Incrementation>()
284-
285-
val javaClazz = testAndLoadClass(clazz)
286-
val method = javaClazz.methods.first { it.name == "iincArrayIntIdx" }
287-
val res = method.invoke(null)
288-
assertArrayEquals(intArrayOf(1, 0, 2), res as IntArray)
289-
}
290-
291-
@Test
292-
fun `iinc arrayByteIdx should work`() {
293-
val clazz = cp.findClass<Incrementation>()
294-
295-
val javaClazz = testAndLoadClass(clazz)
296-
val method = javaClazz.methods.first { it.name == "iincArrayByteIdx" }
297-
val res = method.invoke(null)
298-
assertArrayEquals(intArrayOf(1, 0, 2), res as IntArray)
299-
}
300-
301-
@Test
302-
fun `iinc for`() {
303-
val clazz = cp.findClass<Incrementation>()
304-
305-
val javaClazz = testAndLoadClass(clazz)
306-
val method = javaClazz.methods.first { it.name == "iincFor" }
307-
val res = method.invoke(null)
308-
assertArrayEquals(intArrayOf(0, 1, 2, 3, 4), res as IntArray)
309-
}
310-
311270
}
312271

313272
fun JcMethod.dumpInstructions(): String {

0 commit comments

Comments
 (0)