Skip to content

Commit

Permalink
Add a Code Model
Browse files Browse the repository at this point in the history
This makes it easier to detect jumps and toggle show-line-number
  • Loading branch information
alonalbert committed May 6, 2024
1 parent 5961cef commit ccde85e
Show file tree
Hide file tree
Showing 14 changed files with 558 additions and 283 deletions.
94 changes: 15 additions & 79 deletions src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Disassembly.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@

package dev.romainguy.kotlin.explorer

import dev.romainguy.kotlin.explorer.code.CodeContent
import dev.romainguy.kotlin.explorer.code.CodeContent.Error
import dev.romainguy.kotlin.explorer.dex.DexDumpParser
import dev.romainguy.kotlin.explorer.oat.OatDumpParser
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.currentCoroutineContext
Expand All @@ -29,11 +32,14 @@ import kotlin.io.path.extension
private const val TotalSteps = 5
private const val Done = 1f

private val dexDumpParser = DexDumpParser()
private val oatDumpParser = OatDumpParser()

suspend fun disassemble(
toolPaths: ToolPaths,
source: String,
onDex: (String) -> Unit,
onOat: (String) -> Unit,
onDex: (CodeContent) -> Unit,
onOat: (CodeContent) -> Unit,
onStatusUpdate: (String, Float) -> Unit,
optimize: Boolean
) = coroutineScope {
Expand All @@ -57,7 +63,7 @@ suspend fun disassemble(

if (kotlinc.exitCode != 0) {
launch(ui) {
onDex(kotlinc.output.replace(path.parent.toString() + "/", ""))
onDex(Error(kotlinc.output.replace(path.parent.toString() + "/", "")))
onStatusUpdate("Ready", Done)
}
return@launch
Expand All @@ -77,7 +83,7 @@ suspend fun disassemble(

if (r8.exitCode != 0) {
launch(ui) {
onDex(r8.output)
onDex(Error(r8.output))
onStatusUpdate("Ready", Done)
}
return@launch
Expand All @@ -94,14 +100,14 @@ suspend fun disassemble(

if (dexdump.exitCode != 0) {
launch(ui) {
onDex(dexdump.output)
onDex(Error(dexdump.output))
onStatusUpdate("Ready", Done)
}
return@launch
}

launch(ui) {
onDex(DexDumpParser(dexdump.output).parseDexDump())
onDex(dexDumpParser.parse(dexdump.output))
onStatusUpdate("AOT compilation…", step++ / TotalSteps)
}

Expand All @@ -115,7 +121,7 @@ suspend fun disassemble(

if (push.exitCode != 0) {
launch(ui) {
onOat(push.output)
onOat(Error(push.output))
onStatusUpdate("Ready", Done)
}
return@launch
Expand All @@ -132,7 +138,7 @@ suspend fun disassemble(

if (dex2oat.exitCode != 0) {
launch(ui) {
onOat(dex2oat.output)
onOat(Error(dex2oat.output))
onStatusUpdate("Ready", Done)
}
return@launch
Expand All @@ -148,7 +154,7 @@ suspend fun disassemble(
directory = directory
)

launch(ui) { onOat(filterOat(oatdump.output)) }
launch(ui) { onOat(oatDumpParser.parse(oatdump.output)) }

if (oatdump.exitCode != 0) {
launch(ui) { onStatusUpdate("Ready", Done) }
Expand Down Expand Up @@ -256,73 +262,3 @@ private fun cleanupClasses(directory: Path) {

internal val BuiltInKotlinClass = Regex("^(kotlin|kotlinx|java|javax|org\\.(intellij|jetbrains))\\..+")

private val OatClassNamePattern = Regex("^\\d+: L([^;]+); \\(offset=[0-9a-zA-Zx]+\\) \\(type_idx=\\d+\\).+")
private val OatMethodPattern = Regex("^\\s+\\d+:\\s+(.+)\\s+\\(dex_method_idx=\\d+\\)")
private val OatCodePattern = Regex("^\\s+(0x[a-zA-Z0-9]+):\\s+[a-zA-Z0-9]+\\s+(.+)")

private fun filterOat(oat: String) = buildString {
val indent = " "
val lines = oat.lineSequence().iterator()

if (!lines.consumeUntil("OatDexFile:")) return@buildString

var insideClass = false
var insideMethod = false
var firstMethod = false
var firstClass = true

while (lines.hasNext()) {
val line = lines.next()

var match: MatchResult? = null

if (insideClass) {
if (insideMethod) {
match = OatCodePattern.matchEntire(line)
if (match != null && match.groupValues.isNotEmpty()) {
appendLine("$indent${match.groupValues[1]}: ${match.groupValues[2]}")
}
}

if (match === null) {
match = OatMethodPattern.matchEntire(line)
if (match != null && match.groupValues.isNotEmpty()) {
val name = match.groupValues[1]
if (!firstMethod) appendLine()
firstMethod = false

appendLine(" $name")

if (!lines.consumeUntil("CODE: ")) break
insideMethod = true
}
}
}

if (match === null) {
match = OatClassNamePattern.matchEntire(line)
if (match != null && match.groupValues.isNotEmpty()) {
val className = match.groupValues[1].replace('/', '.')

val suppress = className.matches(BuiltInKotlinClass)
if (!suppress) {
if (!firstClass) appendLine()
appendLine("class $className")
}

insideMethod = false
firstMethod = true
firstClass = false
insideClass = !suppress
}
}
}
}

internal fun Iterator<String>.consumeUntil(prefix: String): Boolean {
while (hasNext()) {
val line = next()
if (line.trim().startsWith(prefix)) return true
}
return false
}
66 changes: 38 additions & 28 deletions src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/KotlinExplorer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@ import androidx.compose.ui.window.*
import androidx.compose.ui.window.WindowPosition.Aligned
import dev.romainguy.kotlin.explorer.Shortcut.Ctrl
import dev.romainguy.kotlin.explorer.Shortcut.CtrlShift
import dev.romainguy.kotlin.explorer.dex.DexTextArea
import dev.romainguy.kotlin.explorer.oat.OatTextArea
import dev.romainguy.kotlin.explorer.code.CodeBuilder.LineNumberMode.FixedWidth
import dev.romainguy.kotlin.explorer.code.CodeBuilder.LineNumberMode.None
import dev.romainguy.kotlin.explorer.code.CodeContent
import dev.romainguy.kotlin.explorer.code.CodeTextArea
import kotlinx.coroutines.launch
import org.fife.rsta.ui.search.FindDialog
import org.fife.rsta.ui.search.SearchEvent
Expand Down Expand Up @@ -70,6 +72,8 @@ import javax.swing.SwingUtilities

private const val FontSizeEditingMode = 12.0f
private const val FontSizePresentationMode = 20.0f
private const val LINE_NUMBER_WIDTH = 4
private const val CODE_INDENT = 4

@Composable
private fun FrameWindowScope.KotlinExplorer(
Expand Down Expand Up @@ -128,27 +132,29 @@ private fun FrameWindowScope.KotlinExplorer(
val dexPanel: @Composable () -> Unit = { TextPanel("DEX", dexTextArea, explorerState) }
val oatPanel: @Composable () -> Unit = { TextPanel("OAT", oatTextArea, explorerState) }
var panels by remember { mutableStateOf(explorerState.getPanels(sourcePanel, dexPanel, oatPanel)) }

val updatePresentationMode: (Boolean) -> Unit = {
listOf(dexTextArea, oatTextArea).forEach { area -> area.presentationMode = it }
}
val updateShowLineNumbers: (Boolean) -> Unit = { dexTextArea.lineNumberMode = it.toLineNumberMode() }

val onProgressUpdate: (String, Float) -> Unit = { newStatus: String, newProgress: Float ->
status = newStatus
progress = newProgress
}

MainMenu(
explorerState,
sourceTextArea,
{ dex ->
if (dex != null) {
updateTextArea(dexTextArea, dex)
} else {
dexTextArea.refreshText()
}
},
{ oat -> updateTextArea(oatTextArea, oat) },
{ dex -> dexTextArea.content = dex },
{ oat -> oatTextArea.content = oat },
onProgressUpdate,
{ findDialog.isVisible = true },
{ SearchEngine.find(activeTextArea, findDialog.searchContext) },
{ showSettings = true },
{ panels = explorerState.getPanels(sourcePanel, dexPanel, oatPanel) },
updateShowLineNumbers,
updatePresentationMode,
)

if (showSettings) {
Expand Down Expand Up @@ -243,15 +249,19 @@ private fun sourceTextArea(focusTracker: FocusListener, explorerState: ExplorerS
}
}

private fun dexTextArea(explorerState: ExplorerState, focusTracker: FocusListener): DexTextArea {
return DexTextArea(explorerState).apply {
configureSyntaxTextArea(SyntaxConstants.SYNTAX_STYLE_NONE)
addFocusListener(focusTracker)
}
}
private fun dexTextArea(state: ExplorerState, focusTracker: FocusListener) =
codeTextArea(state, focusTracker)

private fun oatTextArea(explorerState: ExplorerState, focusTracker: FocusListener): RSyntaxTextArea {
return OatTextArea(explorerState).apply {
private fun oatTextArea(state: ExplorerState, focusTracker: FocusListener) =
codeTextArea(state, focusTracker, hasLineNumbers = false)

private fun codeTextArea(
state: ExplorerState,
focusTracker: FocusListener,
hasLineNumbers: Boolean = true
): CodeTextArea {
val linNumberMode = (hasLineNumbers && state.showLineNumbers).toLineNumberMode()
return CodeTextArea(state.presentationMode, CODE_INDENT, linNumberMode).apply {
configureSyntaxTextArea(SyntaxConstants.SYNTAX_STYLE_NONE)
addFocusListener(focusTracker)
}
Expand All @@ -261,13 +271,15 @@ private fun oatTextArea(explorerState: ExplorerState, focusTracker: FocusListene
private fun FrameWindowScope.MainMenu(
explorerState: ExplorerState,
sourceTextArea: RSyntaxTextArea,
onDexUpdate: (String?) -> Unit,
onOatUpdate: (String) -> Unit,
onDexUpdate: (CodeContent) -> Unit,
onOatUpdate: (CodeContent) -> Unit,
onStatusUpdate: (String, Float) -> Unit,
onFindClicked: () -> Unit,
onFindNextClicked: () -> Unit,
onOpenSettings: () -> Unit,
onPanelsUpdated: () -> Unit,
onShowLineNumberChanged: (Boolean) -> Unit,
onPresentationModeChanged: (Boolean) -> Unit,
) {
val scope = rememberCoroutineScope()

Expand Down Expand Up @@ -296,10 +308,12 @@ private fun FrameWindowScope.MainMenu(
MenuCheckboxItem("Show DEX", Ctrl(D), explorerState::showDex, onShowPanelChanged)
MenuCheckboxItem("Show OAT", Ctrl(O), explorerState::showOat, onShowPanelChanged)
MenuCheckboxItem("Show Line Numbers", CtrlShift(L), explorerState::showLineNumbers) {
onDexUpdate(null)
onShowLineNumberChanged(it)
}
Separator()
MenuCheckboxItem("Presentation Mode", CtrlShift(P), explorerState::presentationMode)
MenuCheckboxItem("Presentation Mode", CtrlShift(P), explorerState::presentationMode) {
onPresentationModeChanged(it)
}
}
Menu("Compilation") {
MenuCheckboxItem("Optimize with R8", CtrlShift(O), explorerState::optimize)
Expand Down Expand Up @@ -334,11 +348,7 @@ private fun RSyntaxTextArea.updateStyle(explorerState: ExplorerState) {
font = font.deriveFont(if (presentation) FontSizePresentationMode else FontSizeEditingMode)
}

private fun updateTextArea(textArea: RSyntaxTextArea, text: String) {
val position = textArea.caretPosition
textArea.text = text
textArea.caretPosition = minOf(position, textArea.document.length)
}
private fun Boolean.toLineNumberMode() = if (this) FixedWidth(LINE_NUMBER_WIDTH) else None

fun main() = application {
val explorerState = remember { ExplorerState() }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright (C) 2024 Romain Guy
*
* 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 dev.romainguy.kotlin.explorer

/** Based on Guava PeekingIterator */
class PeekingIterator<E : Any>(private val iterator: Iterator<E>) : Iterator<E> {
private var peekedElement: E? = null

override fun hasNext(): Boolean {
return peekedElement != null || iterator.hasNext()
}

override fun next(): E {
val element = peekedElement ?: return iterator.next()
peekedElement = null
return element
}

fun peek(): E {
return peekedElement.takeIf { it != null } ?: iterator.next().also { peekedElement = it }
}
}
2 changes: 2 additions & 0 deletions src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Regex.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package dev.romainguy.kotlin.explorer

const val HexDigit = "[0-9a-fA-F]"

fun MatchResult.getValue(group: String): String {
return groups[group]?.value ?: throw IllegalStateException("Value of $group not found in $value")
}
36 changes: 36 additions & 0 deletions src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/String.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2024 The Android Open Source Project
*
* 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 dev.romainguy.kotlin.explorer

fun Iterator<String>.consumeUntil(prefix: String): Boolean {
while (hasNext()) {
val line = next()
if (line.trim().startsWith(prefix)) return true
}
return false
}

fun Iterator<String>.consumeUntil(regex: Regex): MatchResult? {
while (hasNext()) {
val line = next()
val match = regex.matchEntire(line)
if (match != null) {
return match
}
}
return null
}
Loading

0 comments on commit ccde85e

Please sign in to comment.