Skip to content

Commit

Permalink
Add JVM bytecode generation for Kotlin/JVM
Browse files Browse the repository at this point in the history
  • Loading branch information
zhelenskiy committed Dec 28, 2023
1 parent 1fd8b0e commit 4825f9e
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.compiler.server.compiler.components

import com.compiler.server.executor.CommandLineArgument
import com.compiler.server.executor.JavaExecutor
import com.compiler.server.model.ExecutionResult
import com.compiler.server.model.JvmExecutionResult
import com.compiler.server.model.OutputDirectory
import com.compiler.server.model.bean.LibrariesFile
import com.compiler.server.model.toExceptionDescriptor
Expand All @@ -16,9 +16,12 @@ import org.jetbrains.org.objectweb.asm.ClassReader.*
import org.jetbrains.org.objectweb.asm.ClassVisitor
import org.jetbrains.org.objectweb.asm.MethodVisitor
import org.jetbrains.org.objectweb.asm.Opcodes.*
import org.jetbrains.org.objectweb.asm.util.TraceClassVisitor
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component
import java.io.File
import java.io.PrintWriter
import java.io.StringWriter
import java.nio.file.FileVisitResult
import java.nio.file.Files
import java.nio.file.Path
Expand All @@ -38,16 +41,34 @@ class KotlinCompiler(
val mainClasses: Set<String> = emptySet()
)

fun run(files: List<KtFile>, args: String): ExecutionResult {
private fun ByteArray.asHumanReadable(): String {
val classReader = ClassReader(this)
val stringWriter = StringWriter()
val printWriter = PrintWriter(stringWriter)
val traceClassVisitor = TraceClassVisitor(printWriter)

classReader.accept(traceClassVisitor, 0)

return stringWriter.toString()
}

private fun JvmExecutionResult.addByteCode(compiled: JvmClasses) {
jvmByteCode = compiled.files
.mapNotNull { (_, bytes) -> runCatching { bytes.asHumanReadable() }.getOrNull() }
.takeUnless { it.isEmpty() }
?.joinToString("\n\n")
}

fun run(files: List<KtFile>, args: String): JvmExecutionResult {
return execute(files) { output, compiled ->
val mainClass = JavaRunnerExecutor::class.java.name
val compiledMainClass = when (compiled.mainClasses.size) {
0 -> return@execute ExecutionResult(
0 -> return@execute JvmExecutionResult(
exception = IllegalArgumentException("No main method found in project").toExceptionDescriptor()
)

1 -> compiled.mainClasses.single()
else -> return@execute ExecutionResult(
else -> return@execute JvmExecutionResult(
exception = IllegalArgumentException(
"Multiple classes in project contain main methods found: ${compiled.mainClasses.joinToString()}"
).toExceptionDescriptor()
Expand All @@ -59,7 +80,7 @@ class KotlinCompiler(
}
}

fun test(files: List<KtFile>): ExecutionResult {
fun test(files: List<KtFile>): JvmExecutionResult {
return execute(files) { output, _ ->
val mainClass = JUnitExecutors::class.java.name
javaExecutor.execute(argsFrom(mainClass, output, listOf(output.path.toString())))
Expand Down Expand Up @@ -117,22 +138,23 @@ class KotlinCompiler(

private fun execute(
files: List<KtFile>,
block: (output: OutputDirectory, compilation: JvmClasses) -> ExecutionResult
): ExecutionResult = try {
block: (output: OutputDirectory, compilation: JvmClasses) -> JvmExecutionResult
): JvmExecutionResult = try {
when (val compilationResult = compile(files)) {
is Compiled<JvmClasses> -> {
usingTempDirectory { outputDir ->
val output = write(compilationResult.result, outputDir)
block(output, compilationResult.result).also {
it.addWarnings(compilationResult.compilerDiagnostics)
it.addByteCode(compilationResult.result)
}
}
}

is NotCompiled -> ExecutionResult(compilerDiagnostics = compilationResult.compilerDiagnostics)
is NotCompiled -> JvmExecutionResult(compilerDiagnostics = compilationResult.compilerDiagnostics)
}
} catch (e: Exception) {
ExecutionResult(exception = e.toExceptionDescriptor())
JvmExecutionResult(exception = e.toExceptionDescriptor())
}

private fun write(classes: JvmClasses, outputDir: Path): OutputDirectory {
Expand Down
13 changes: 10 additions & 3 deletions src/main/kotlin/com/compiler/server/model/ExecutionResult.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.annotation.JsonSerialize

open class ExecutionResult(
sealed class ExecutionResult(
@field:JsonProperty("errors")
open var compilerDiagnostics: CompilerDiagnostics = CompilerDiagnostics(),
open var exception: ExceptionDescriptor? = null
Expand Down Expand Up @@ -51,6 +51,12 @@ data class CompilerDiagnostics(
val map: Map<String, List<ErrorDescriptor>> = mapOf()
): List<ErrorDescriptor> by map.values.flatten()

open class JvmExecutionResult(
compilerDiagnostics: CompilerDiagnostics = CompilerDiagnostics(),
exception: ExceptionDescriptor? = null,
var jvmByteCode: String? = null,
): ExecutionResult(compilerDiagnostics, exception)

abstract class TranslationResultWithJsCode(
open val jsCode: String?,
compilerDiagnostics: CompilerDiagnostics,
Expand Down Expand Up @@ -79,8 +85,9 @@ class JunitExecutionResult(
val testResults: Map<String, List<TestDescription>> = emptyMap(),
override var exception: ExceptionDescriptor? = null,
@field:JsonProperty("errors")
override var compilerDiagnostics: CompilerDiagnostics = CompilerDiagnostics()
) : ExecutionResult(compilerDiagnostics, exception)
override var compilerDiagnostics: CompilerDiagnostics = CompilerDiagnostics(),
jvmBytecode: String? = null,
) : JvmExecutionResult(compilerDiagnostics, exception, jvmBytecode)

private fun unEscapeOutput(value: String) = value.replace("&amp;lt;".toRegex(), "<")
.replace("&amp;gt;".toRegex(), ">")
Expand Down
15 changes: 8 additions & 7 deletions src/main/kotlin/com/compiler/server/model/ProgramOutput.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,27 @@ const val ERROR_STREAM_END = "</errStream>"

data class ProgramOutput(
val standardOutput: String = "",
val jvmByteCode: String? = null,
val exception: Exception? = null,
val restriction: String? = null
) {
fun asExecutionResult(): ExecutionResult {
fun asExecutionResult(): JvmExecutionResult {
return when {
restriction != null -> ExecutionResult().apply { text = buildRestriction(restriction) }
exception != null -> ExecutionResult(exception = exception.toExceptionDescriptor())
standardOutput.isBlank() -> ExecutionResult()
restriction != null -> JvmExecutionResult().apply { text = buildRestriction(restriction) }
exception != null -> JvmExecutionResult(exception = exception.toExceptionDescriptor())
standardOutput.isBlank() -> JvmExecutionResult()
else -> {
try {
// coroutines can produce incorrect output. see example in `base coroutines test 7`
if (standardOutput.startsWith("{")) outputMapper.readValue(standardOutput, ExecutionResult::class.java)
if (standardOutput.startsWith("{")) outputMapper.readValue(standardOutput, JvmExecutionResult::class.java)
else {
val result = outputMapper.readValue("{" + standardOutput.substringAfter("{"), ExecutionResult::class.java)
val result = outputMapper.readValue("{" + standardOutput.substringAfter("{"), JvmExecutionResult::class.java)
result.apply {
text = standardOutput.substringBefore("{") + text
}
}
} catch (e: Exception) {
ExecutionResult(exception = e.toExceptionDescriptor())
JvmExecutionResult(exception = e.toExceptionDescriptor())
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/test/kotlin/com/compiler/server/CompilerAPITest.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.compiler.server

import com.compiler.server.generator.generateSingleProject
import com.compiler.server.model.ExecutionResult
import com.compiler.server.model.JvmExecutionResult
import com.compiler.server.model.bean.VersionInfo
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.junit.jupiter.api.Test
Expand Down Expand Up @@ -51,7 +51,7 @@ class CompilerAPITest {
),
headers
),
ExecutionResult::class.java
JvmExecutionResult::class.java
)
assertNotNull(response, "Empty response!")
assertContains(
Expand Down
10 changes: 9 additions & 1 deletion src/test/kotlin/com/compiler/server/JvmRunnerTest.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
package com.compiler.server

import com.compiler.server.base.BaseExecutorTest
import com.compiler.server.model.JvmExecutionResult
import org.junit.jupiter.api.Test
import kotlin.test.assertContains
import kotlin.test.assertEquals

class JvmRunnerTest : BaseExecutorTest() {

@Test
fun `base execute test JVM`() {
run(
val executionResult = run(
code = "fun main() {\n println(\"Hello, world!!!\")\n}",
contains = "Hello, world!!!"
)

val byteCode = (executionResult as JvmExecutionResult).jvmByteCode!!
assertContains(byteCode, "public static synthetic main([Ljava/lang/String;)V", message = byteCode)
assertContains(byteCode, "public final static main()V", message = byteCode)
assertContains(byteCode, "LDC \"Hello, world!!!\"", message = byteCode)
assertContains(byteCode, "INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V", message = byteCode)
}

@Test
Expand Down

0 comments on commit 4825f9e

Please sign in to comment.