Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Line Table Parsing in ByteCodeParser #64

Merged
merged 1 commit into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -83,60 +83,82 @@ class ByteCodeParser {
}
}

private fun PeekingIterator<String>.readClass(classHeader: String): Class {
val methods = buildList {
while (hasNext()) {
val line = peek()
when {
line == "}" -> break
MethodRegex.matches(line) -> add(readMethod())
else -> next()
}
}
if (next() != "}") {
throw IllegalStateException("Expected '}' but got '${peek()}'")
}

private fun PeekingIterator<String>.readClass(classHeader: String): Class {
val methods = buildList {
while (hasNext()) {
val line = peek()
when {
line == "}" -> break
MethodRegex.matches(line) -> add(readMethod())
else -> next()
}
}
return Class(classHeader, methods)
if (next() != "}") {
throw IllegalStateException("Expected '}' but got '${peek()}'")
}
}
return Class(classHeader, methods)
}

private fun PeekingIterator<String>.readMethod(): Method {
val match = MethodRegex.matchEntire(next())
?: throw IllegalStateException("Expected method but got '${peek()}'")
val header = match.getValue("header")
if (next().trim() != "Code:") {
throw IllegalStateException("Expected 'Code:' but got '${peek()}'")
}
val instructions = readInstructions()
val lineNumbers = readLineNumbers()

return Method(header, InstructionSet(ISA.ByteCode, instructions.withLineNumbers(lineNumbers)))
private fun PeekingIterator<String>.readMethod(): Method {
val match = MethodRegex.matchEntire(next())
?: throw IllegalStateException("Expected method but got '${peek()}'")
val header = match.getValue("header")
if (next().trim() != "Code:") {
throw IllegalStateException("Expected 'Code:' but got '${peek()}'")
}
val instructions = readInstructions()
val lineNumbers = readLineNumbers()

private fun PeekingIterator<String>.readInstructions(): List<Instruction> {
return buildList {
while (hasNext()) {
val line = next().trim()
val match = InstructionRegex.matchEntire(line) ?: break
val address = match.getValue("address")
val code = match.getValue("code").replace(CommentRegex, " //")
val jumpAddress = JumpRegex.matchEntire(code)?.getValue("address")?.toInt() ?: -1
val (op, operands) = codeToOpAndOperands(code)
add(Instruction(address.toInt(), address, op, operands, jumpAddress))
}
}
}
return Method(header, InstructionSet(ISA.ByteCode, instructions.withLineNumbers(lineNumbers)))
}

private fun PeekingIterator<String>.readLineNumbers(): IntIntMap {
val map = mutableIntIntMapOf()

private fun PeekingIterator<String>.readInstructions(): List<Instruction> {
return buildList {
while (hasNext()) {
val line = next().trim()
if (!line.startsWith("line")) {
break
}
val (lineNumber, address) = line.substringAfter(' ').split(": ", limit = 2)
map.put(address.toInt(), lineNumber.toInt())
val line = peek().trim()
val match = InstructionRegex.matchEntire(line) ?: break
next()
val address = match.getValue("address")
val code = match.getValue("code").replace(CommentRegex, " //")
val jumpAddress = JumpRegex.matchEntire(code)?.getValue("address")?.toInt() ?: -1
val (op, operands) = codeToOpAndOperands(code)
add(Instruction(address.toInt(), address, op, operands, jumpAddress))
}
}
}

private fun PeekingIterator<String>.readLineNumbers(): IntIntMap {
val map = mutableIntIntMapOf()
val found = skipToLineNumberTable()
if (!found) {
return map
}
next()
while (hasNext()) {
val line = peek().trim()
if (!line.startsWith("line")) {
break
}
next()
val (lineNumber, address) = line.substringAfter(' ').split(": ", limit = 2)
map.put(address.toInt(), lineNumber.toInt())
}
return map
}

private fun PeekingIterator<String>.skipToLineNumberTable(): Boolean {
while (hasNext()) {
val line = peek().trim()
when (line) {
"LineNumberTable:" -> return true
"", "}" -> return false
}
next()
}
return false
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@
package dev.romainguy.kotlin.explorer.bytecode

import com.google.common.truth.Truth.assertThat
import dev.romainguy.kotlin.explorer.code.Code
import dev.romainguy.kotlin.explorer.testing.Builder
import dev.romainguy.kotlin.explorer.testing.parseSuccess
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.nio.file.Path
import kotlin.io.path.readText

class ByteCodeParserTest {
@get:Rule
Expand All @@ -34,18 +37,23 @@ class ByteCodeParserTest {
fun issue_45() {
val content = byteCodeParser.parseSuccess(builder.generateByteCode("Issue_45.kt"))

assertThat(content.classes.map { it.header }).containsExactly(
"final class testData.Issue_45Kt\$main$1",
"public final class testData.Issue_45Kt",
)
assertThat(content.classes.flatMap { it.methods }.map { it.header }).containsExactly(
"testData.Issue_45Kt\$main\$1()",
"public final void invoke()",
"public java.lang.Object invoke()",
"public static final void main()",
"public static final void f1(kotlin.jvm.functions.Function0<kotlin.Unit>)",
"public static final void f2()",
"public static void main(java.lang.String[])",
)
val text = Code.fromClasses(content.classes).text

assertThat(text).isEqualTo(loadTestDataFile("Issue_45-Bytecode.expected"))
}

@Test
fun tryCatch() {
val content = byteCodeParser.parseSuccess(builder.generateByteCode("TryCatch.kt"))

val text = Code.fromClasses(content.classes).text

assertThat(text).isEqualTo(loadTestDataFile("TryCatch-Bytecode.expected"))
}
}

fun loadTestDataFile(path: String): String {
val cwd = Path.of(System.getProperty("user.dir"))
val testData = cwd.resolve("src/jvmTest/kotlin/testData")
return testData.resolve(path).readText()
}
47 changes: 47 additions & 0 deletions src/jvmTest/kotlin/testData/Issue_45-Bytecode.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
final class testData.Issue_45Kt$main$1
testData.Issue_45Kt$main$1()
-- 4 instructions
0: aload_0
1: iconst_0
2: invokespecial #12 // Method kotlin/jvm/internal/Lambda."<init>":(I)V
5: return

public final void invoke()
-- 2 instructions
5: 0: invokestatic #20 // Method testData/Issue_45Kt.f2:()V
6: 3: return

public java.lang.Object invoke()
-- 4 instructions
4: 0: aload_0
1: invokevirtual #23 // Method invoke:()V
4: getstatic #29 // Field kotlin/Unit.INSTANCE:Lkotlin/Unit;
7: areturn

public final class testData.Issue_45Kt
public static final void main()
-- 4 instructions
4: 0: getstatic #12 // Field testData/Issue_45Kt$main$1.INSTANCE:LtestData/Issue_45Kt$main$1;
3: checkcast #14 // class kotlin/jvm/functions/Function0
6: invokestatic #18 // Method f1:(Lkotlin/jvm/functions/Function0;)V
7: 9: return

public static final void f1(kotlin.jvm.functions.Function0<kotlin.Unit>)
-- 4 instructions
9: 0: aload_0
1: invokeinterface #24, 1 // InterfaceMethod kotlin/jvm/functions/Function0.invoke:()Ljava/lang/Object;
6: pop
7: return

public static final void f2()
-- 5 instructions
11: 0: ldc #29 // String Hi
2: getstatic #35 // Field java/lang/System.out:Ljava/io/PrintStream;
5: swap
6: invokevirtual #41 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
12: 9: return

public static void main(java.lang.String[])
-- 2 instructions
0: invokestatic #44 // Method main:()V
3: return
23 changes: 23 additions & 0 deletions src/jvmTest/kotlin/testData/TryCatch-Bytecode.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
public final class testData.TryCatchKt
public static final void main()
-- 9 instructions
4: 0: aconst_null
1: astore_0
5: 2: nop
6: 3: invokestatic #11 // Method foo:()V
6: goto 16
8: 9: astore_1
9: 10: getstatic #17 // Field java/lang/System.out:Ljava/io/PrintStream;
13: invokevirtual #22 // Method java/io/PrintStream.println:()V
11: 16: return

public static final void foo()
-- 3 instructions
14: 0: getstatic #17 // Field java/lang/System.out:Ljava/io/PrintStream;
3: invokevirtual #22 // Method java/io/PrintStream.println:()V
15: 6: return

public static void main(java.lang.String[])
-- 2 instructions
0: invokestatic #29 // Method main:()V
3: return
45 changes: 45 additions & 0 deletions src/jvmTest/kotlin/testData/TryCatch.javap
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
Compiled from "TryCatch.kt"
public final class testData.TryCatchKt {
public static final void main();
Code:
0: aconst_null
1: astore_0
2: nop
3: invokestatic #11 // Method foo:()V
6: goto 16
9: astore_1
10: getstatic #17 // Field java/lang/System.out:Ljava/io/PrintStream;
13: invokevirtual #22 // Method java/io/PrintStream.println:()V
16: return
Exception table:
from to target type
2 6 9 Class java/lang/Exception
LineNumberTable:
line 4: 0
line 5: 2
line 6: 3
line 8: 9
line 9: 10
line 11: 16
LocalVariableTable:
Start Length Slot Name Signature
10 6 1 e Ljava/lang/Exception;
2 15 0 a Ljava/lang/Void;

public static final void foo();
Code:
0: getstatic #17 // Field java/lang/System.out:Ljava/io/PrintStream;
3: invokevirtual #22 // Method java/io/PrintStream.println:()V
6: return
LineNumberTable:
line 14: 0
line 15: 6

public static void main(java.lang.String[]);
Code:
0: invokestatic #29 // Method main:()V
3: return
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 args [Ljava/lang/String;
}
15 changes: 15 additions & 0 deletions src/jvmTest/kotlin/testData/TryCatch.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package testData

fun main() {
val a = null
try {
foo()
}
catch (e: Exception) {
println()
}
}

fun foo() {
println()
}
Loading