├── .github
└── workflows
│ ├── presubmit.yml
│ └── release.yml
├── .gitignore
├── .idea
├── .gitignore
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── gradle.xml
├── kotlinc.xml
├── misc.xml
└── vcs.xml
├── LICENSE
├── README.md
├── art
├── app-icon
│ ├── icon.icns
│ ├── icon.ico
│ ├── icon.iconset
│ │ ├── icon_128x128.png
│ │ ├── icon_128x128@2x.png
│ │ ├── icon_16x16.png
│ │ ├── icon_16x16@2x.png
│ │ ├── icon_256x256.png
│ │ ├── icon_256x256@2x.png
│ │ ├── icon_32x32.png
│ │ ├── icon_32x32@2x.png
│ │ ├── icon_512x512.png
│ │ └── icon_512x512@2x.png
│ └── kotlin-explorer.afphoto
└── kotlin-explorer.png
├── build.gradle.kts
├── compose-desktop.pro
├── compose-stability.config
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── scripts
├── .gitignore
├── aarch64.json
└── help-generator-arm.py
├── settings.gradle.kts
├── src
├── jvmMain
│ ├── kotlin
│ │ └── dev
│ │ │ └── romainguy
│ │ │ └── kotlin
│ │ │ └── explorer
│ │ │ ├── DependencyCache.kt
│ │ │ ├── Disassembly.kt
│ │ │ ├── DocumentChangeListener.kt
│ │ │ ├── KotlinExplorer.kt
│ │ │ ├── Logger.kt
│ │ │ ├── Menu.kt
│ │ │ ├── OS.kt
│ │ │ ├── Paths.kt
│ │ │ ├── PeekingIterator.kt
│ │ │ ├── Process.kt
│ │ │ ├── ProgressUpdater.kt
│ │ │ ├── Regex.kt
│ │ │ ├── Settings.kt
│ │ │ ├── SourceTextArea.kt
│ │ │ ├── Splitter.kt
│ │ │ ├── State.kt
│ │ │ ├── String.kt
│ │ │ ├── Swing.kt
│ │ │ ├── SyntaxTextArea.kt
│ │ │ ├── Theme.kt
│ │ │ ├── build
│ │ │ ├── ByteCodeDecompiler.kt
│ │ │ ├── DexCompiler.kt
│ │ │ └── KolinCompiler.kt
│ │ │ ├── bytecode
│ │ │ └── ByteCodeParser.kt
│ │ │ ├── code
│ │ │ ├── Aarch64Docs.kt
│ │ │ ├── Code.kt
│ │ │ ├── CodeBuilder.kt
│ │ │ ├── CodeContent.kt
│ │ │ ├── CodeStyle.kt
│ │ │ ├── CodeTextArea.kt
│ │ │ ├── DataModels.kt
│ │ │ ├── Documentation.kt
│ │ │ ├── OpCodeDoc.kt
│ │ │ └── SyntaxStyle.kt
│ │ │ ├── dex
│ │ │ └── DexDumpParser.kt
│ │ │ └── oat
│ │ │ └── OatDumpParser.kt
│ └── resources
│ │ ├── icons
│ │ ├── done.svg
│ │ ├── error.svg
│ │ └── icon.ico
│ │ └── themes
│ │ ├── kotlin_explorer.xml
│ │ └── kotlin_explorer_disassembly.xml
└── jvmTest
│ └── kotlin
│ ├── dev
│ └── romainguy
│ │ └── kotlin
│ │ └── explorer
│ │ ├── bytecode
│ │ └── ByteCodeParserTest.kt
│ │ └── testing
│ │ └── Builder.kt
│ └── testData
│ ├── Issue_45-Bytecode.expected
│ ├── Issue_45.javap
│ ├── Issue_45.kt
│ ├── TryCatch-Bytecode.expected
│ ├── TryCatch.javap
│ └── TryCatch.kt
└── token-makers
├── build.gradle.kts
└── src
└── main
└── java
└── dev
└── romainguy
└── kotlin
└── explorer
└── code
├── DexTokenMaker.flex
├── DexTokenMaker.java
├── KotlinTokenMaker.flex
├── KotlinTokenMaker.java
├── OatTokenMaker.flex
└── OatTokenMaker.java
/.github/workflows/presubmit.yml:
--------------------------------------------------------------------------------
1 | name: Presubmit
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | build:
13 | runs-on: macos-latest
14 | steps:
15 | - uses: actions/checkout@v4
16 | # TODO: replace this once https://github.com/actions/setup-java/pull/637 gets merged.
17 | - uses: gmitch215/setup-java@6d2c5e1f82f180ae79f799f0ed6e3e5efb4e664d
18 | with:
19 | distribution: 'jetbrains'
20 | java-version: 17
21 | env:
22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
23 | - uses: gradle/actions/setup-gradle@v3
24 | - run: ./gradlew build
25 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - '**'
7 |
8 | jobs:
9 | build:
10 | strategy:
11 | matrix:
12 | # The only available image for x86 is macos-13-large
13 | # https://github.com/actions/runner-images?tab=readme-ov-file#available-images
14 | os: [ macos-13, macos-latest ]
15 | runs-on: ${{ matrix.os }}
16 | steps:
17 | - uses: actions/checkout@v4
18 | # TODO: replace this once https://github.com/actions/setup-java/pull/637 gets merged.
19 | - uses: gmitch215/setup-java@6d2c5e1f82f180ae79f799f0ed6e3e5efb4e664d
20 | with:
21 | distribution: 'jetbrains'
22 | java-version: 17
23 | env:
24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
25 | - uses: gradle/actions/setup-gradle@v3
26 | - run: ./gradlew build
27 | - uses: actions/upload-artifact@v4
28 | with:
29 | name: distribution-${{ matrix.os }}
30 | if-no-files-found: error
31 | path: |
32 | build/compose/binaries/main-release/dmg/kotlin-explorer-*.dmg
33 |
34 | release:
35 | runs-on: ubuntu-latest
36 | if: github.repository_owner == 'romainguy'
37 | needs: build
38 | permissions:
39 | contents: write
40 | steps:
41 | - uses: actions/checkout@v4
42 | - uses: actions/download-artifact@v4
43 | - name: Create release and upload dists
44 | env:
45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
46 | run: |
47 | gh release create "${{ github.ref_name }}" **/*.dmg -t "${{ github.ref_name }}" --draft --generate-notes
48 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | build/
3 | !gradle/wrapper/gradle-wrapper.jar
4 | !**/src/**/build/
5 |
6 | ### IntelliJ IDEA ###
7 | .idea/modules.xml
8 | .idea/jarRepositories.xml
9 | .idea/compiler.xml
10 | .idea/uiDesigner.xml
11 | .idea/libraries/
12 | .idea/artifacts/
13 | *.iws
14 | *.iml
15 | *.ipr
16 | out/
17 | !**/src/main/**/out/
18 | !**/src/test/**/out/
19 |
20 | ### Eclipse ###
21 | .apt_generated
22 | .classpath
23 | .factorypath
24 | .project
25 | .settings
26 | .springBeans
27 | .sts4-cache
28 | bin/
29 | !**/src/main/**/bin/
30 | !**/src/test/**/bin/
31 |
32 | ### NetBeans ###
33 | /nbproject/private/
34 | /nbbuild/
35 | /dist/
36 | /nbdist/
37 | /.nb-gradle/
38 |
39 | ### VS Code ###
40 | .vscode/
41 |
42 | ### Mac OS ###
43 | .DS_Store
44 | /local.properties
45 | /.idea/AndroidProjectSystem.xml
46 | /.idea/inspectionProfiles/Project_Default.xml
47 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
18 |
19 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Kotlin Explorer
2 | 
3 |
4 | Kotlin Explorer is a desktop tool to quickly and easily disassemble Kotlin code into:
5 | - Java bytecode
6 | - Android DEX bytecode
7 | - Android OAT assembly
8 |
9 | After launching Kotlin Explorer, type valid Kotlin code in the left pane, then click
10 | *Build > Build & Disassemble* or use `Cmd-Shift-D` on macOS, `Ctrl-Shift-D`
11 | on Linux and Windows.
12 |
13 | By default, the middle pane will show the Android DEX bytecode, and the right panel
14 | the native assembly resulting from ahead of time compilation (AOT). You can control
15 | which panels are visible using the *View* menu.
16 |
17 | 
18 |
19 | # Features
20 |
21 | - *Build > Optimize with R8*: turn on R8 optimizations. Turning this on will affect the
22 | ability to see corresponding source line numbers in the byte code and DEX outputs.
23 | - *View > Sync Lines*: synchronize the current line in the source, byte code, and DEX
24 | panels. This feature may require R8 optimizations to be turned off to work properly.
25 | - *View > Presentation Mode*: increase the font size to make the content more visible
26 | when projected.
27 | - *Build > Build on Startup*: to automatically launch a compilation when launching the
28 | app.
29 | - *Build > Run*: compile the Kotlin source code and run it locally. Any output is sent
30 | to the logs panel.
31 | - Clicking a jump instruction will show an arrow to the jump destination.
32 | - Shows the number of instructions and branches per method.
33 | - Click a disassembled instruction or register to highlight all occurrences.
34 | - Inline aarch64 (ARM 64 bit) documentation. Use *View > Show Logs & Documentation*.
35 |
36 | # Kotlin Explorer and R8
37 |
38 | You can use *Build > Optimize with R8* to optimize the compiled code with the R8 tool.
39 | By default, all public classes/members/etc. will be kept, allowing you to analyze them
40 | in the disassembly panels.
41 |
42 | However, keeping everything is not representative of what R8 will do on an actual
43 | application so you can disable that feature, and instead use the `@Keep` annotation
44 | to choose an entry point leading to the desired disassembly. You can also create a
45 | `fun main()` entry point and call your code from there. Be careful to not use constants
46 | when calling other methods as this may lead to aggressive optimization by R8.
47 |
48 | # Running Kotlin Explorer
49 |
50 | Run Kotlin Explorer with `./gradlew jvmRun`.
51 |
52 | Kotlin Explorer needs to be told where to find the Android SDK and the Kotlin compiler.
53 | Unless you've set `$ANDROID_HOME` and `$KOTLIN_HOME` properly, Kotlin Explorer will ask
54 | you to enter the path to those directories.
55 |
56 | For `$ANDROID_HOME`, use the path to the root of the Android SDK (directory containing
57 | `build-tools/`, `platform-tools/`, etc.). Android Studio for macOS stores this in
58 | `$HOME/Library/Android/sdk`.
59 |
60 | For `$KOTLIN_HOME`, use the path to the root of your
61 | [Kotlin installation](https://kotlinlang.org/docs/command-line.html). This directory
62 | should contain `bin/kotlinc` and `lib/kotlin-stdlib-*.jar` for instance.
63 |
64 | Kotlin explorer also requires `java` and `javap` to be in your `$PATH`.
65 |
66 | > [!IMPORTANT]
67 | > DEX bytecode and OAT assembly will only be displayed if you have an Android
68 | > device or emulator that can be successfully reached via `adb`. The device
69 | > must be recent enough to host the `oatdump` tool on its system image.
70 |
71 | # License
72 |
73 | Please see [LICENSE](./LICENSE).
74 |
--------------------------------------------------------------------------------
/art/app-icon/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romainguy/kotlin-explorer/d783c4acc16ac82655c41760568f3be6181db9c4/art/app-icon/icon.icns
--------------------------------------------------------------------------------
/art/app-icon/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romainguy/kotlin-explorer/d783c4acc16ac82655c41760568f3be6181db9c4/art/app-icon/icon.ico
--------------------------------------------------------------------------------
/art/app-icon/icon.iconset/icon_128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romainguy/kotlin-explorer/d783c4acc16ac82655c41760568f3be6181db9c4/art/app-icon/icon.iconset/icon_128x128.png
--------------------------------------------------------------------------------
/art/app-icon/icon.iconset/icon_128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romainguy/kotlin-explorer/d783c4acc16ac82655c41760568f3be6181db9c4/art/app-icon/icon.iconset/icon_128x128@2x.png
--------------------------------------------------------------------------------
/art/app-icon/icon.iconset/icon_16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romainguy/kotlin-explorer/d783c4acc16ac82655c41760568f3be6181db9c4/art/app-icon/icon.iconset/icon_16x16.png
--------------------------------------------------------------------------------
/art/app-icon/icon.iconset/icon_16x16@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romainguy/kotlin-explorer/d783c4acc16ac82655c41760568f3be6181db9c4/art/app-icon/icon.iconset/icon_16x16@2x.png
--------------------------------------------------------------------------------
/art/app-icon/icon.iconset/icon_256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romainguy/kotlin-explorer/d783c4acc16ac82655c41760568f3be6181db9c4/art/app-icon/icon.iconset/icon_256x256.png
--------------------------------------------------------------------------------
/art/app-icon/icon.iconset/icon_256x256@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romainguy/kotlin-explorer/d783c4acc16ac82655c41760568f3be6181db9c4/art/app-icon/icon.iconset/icon_256x256@2x.png
--------------------------------------------------------------------------------
/art/app-icon/icon.iconset/icon_32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romainguy/kotlin-explorer/d783c4acc16ac82655c41760568f3be6181db9c4/art/app-icon/icon.iconset/icon_32x32.png
--------------------------------------------------------------------------------
/art/app-icon/icon.iconset/icon_32x32@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romainguy/kotlin-explorer/d783c4acc16ac82655c41760568f3be6181db9c4/art/app-icon/icon.iconset/icon_32x32@2x.png
--------------------------------------------------------------------------------
/art/app-icon/icon.iconset/icon_512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romainguy/kotlin-explorer/d783c4acc16ac82655c41760568f3be6181db9c4/art/app-icon/icon.iconset/icon_512x512.png
--------------------------------------------------------------------------------
/art/app-icon/icon.iconset/icon_512x512@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romainguy/kotlin-explorer/d783c4acc16ac82655c41760568f3be6181db9c4/art/app-icon/icon.iconset/icon_512x512@2x.png
--------------------------------------------------------------------------------
/art/app-icon/kotlin-explorer.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romainguy/kotlin-explorer/d783c4acc16ac82655c41760568f3be6181db9c4/art/app-icon/kotlin-explorer.afphoto
--------------------------------------------------------------------------------
/art/kotlin-explorer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romainguy/kotlin-explorer/d783c4acc16ac82655c41760568f3be6181db9c4/art/kotlin-explorer.png
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage")
2 |
3 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat
4 | import org.jetbrains.compose.desktop.application.tasks.AbstractJPackageTask
5 | import kotlin.io.path.listDirectoryEntries
6 |
7 | plugins {
8 | alias(libs.plugins.kotlin.multiplatform)
9 | alias(libs.plugins.jetbrains.compose)
10 | alias(libs.plugins.compose.compiler)
11 | }
12 |
13 | repositories {
14 | google()
15 | mavenCentral()
16 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
17 | maven("https://packages.jetbrains.team/maven/p/kpm/public/")
18 | }
19 |
20 | version = "1.6.6"
21 | val baseName = "Kotlin Explorer"
22 |
23 | kotlin {
24 | jvm {
25 | @Suppress("OPT_IN_USAGE")
26 | mainRun {
27 | mainClass = "dev.romainguy.kotlin.explorer.KotlinExplorerKt"
28 | }
29 | }
30 |
31 | jvmToolchain {
32 | vendor = JvmVendorSpec.JETBRAINS
33 | languageVersion = JavaLanguageVersion.of(17)
34 | }
35 |
36 | sourceSets {
37 | val jvmMain by getting {
38 | dependencies {
39 | implementation(compose.desktop.currentOs) {
40 | exclude(group = "org.jetbrains.compose.material")
41 | }
42 | implementation(compose.components.resources)
43 | implementation(libs.collection)
44 | implementation(libs.compose.backhandler)
45 | implementation(libs.compose.material3)
46 | implementation(libs.compose.splitpane)
47 | implementation(libs.jewel)
48 | implementation(libs.jewel.decorated)
49 | implementation(libs.jewel.markdown.core)
50 | implementation(libs.jewel.markdown.intUiStandaloneStyling)
51 | implementation(libs.jna)
52 | implementation(libs.lifecycle)
53 | implementation(libs.lifecycle.compose)
54 | implementation(libs.lifecycle.viewmodel)
55 | implementation(libs.lifecycle.viewmodel.compose)
56 | implementation(libs.skiko.mac)
57 | implementation(libs.skiko.linux)
58 | implementation(libs.rsyntaxtextarea)
59 | implementation(libs.rstaui)
60 | implementation(project(":token-makers"))
61 | }
62 | }
63 |
64 | val jvmTest by getting {
65 | dependencies {
66 | implementation(libs.junit4)
67 | implementation(libs.kotlin.test)
68 | implementation(libs.truth)
69 | }
70 | }
71 | }
72 | }
73 |
74 | composeCompiler {
75 | stabilityConfigurationFiles = listOf(layout.projectDirectory.file("compose-stability.config"))
76 | reportsDestination = layout.buildDirectory.dir("compose-compiler")
77 | }
78 |
79 | compose.desktop {
80 | application {
81 | mainClass = "dev.romainguy.kotlin.explorer.KotlinExplorerKt"
82 | buildTypes.release.proguard {
83 | configurationFiles.from(project.file("compose-desktop.pro"))
84 | }
85 | nativeDistributions {
86 | modules("jdk.unsupported")
87 |
88 | targetFormats(TargetFormat.Dmg, TargetFormat.Exe)
89 |
90 | packageVersion = version.toString()
91 | packageName = baseName
92 | description = baseName
93 | vendor = "Romain Guy"
94 | licenseFile = rootProject.file("LICENSE")
95 |
96 | macOS {
97 | dockName = "Kotlin Explorer"
98 | iconFile = file("art/app-icon/icon.icns")
99 | bundleID = "dev.romainguy.kotlin.explorer"
100 | }
101 |
102 | windows {
103 | menuGroup = "Kotlin Explorer"
104 | iconFile = file("art/app-icon/icon.ico")
105 | }
106 | }
107 | }
108 | }
109 |
110 | val currentArch: String = when (val osArch = System.getProperty("os.arch")) {
111 | "x86_64", "amd64" -> "x64"
112 | "aarch64" -> "arm64"
113 | else -> error("Unsupported OS arch: $osArch")
114 | }
115 |
116 | /**
117 | * TODO: workaround for https://github.com/JetBrains/compose-multiplatform/issues/4976.
118 | */
119 | val renameDmg by tasks.registering(Copy::class) {
120 | group = "distribution"
121 | description = "Rename the DMG file"
122 |
123 | val packageReleaseDmg = tasks.named("packageReleaseDmg")
124 | // build/compose/binaries/main-release/dmg/*.dmg
125 | val fromFile = packageReleaseDmg.map { task ->
126 | task.destinationDir.asFile.get().toPath().listDirectoryEntries("$baseName*.dmg").single()
127 | }
128 |
129 | from(fromFile)
130 | into(fromFile.map { it.parent })
131 | rename {
132 | "kotlin-explorer-$currentArch-$version.dmg"
133 | }
134 | }
135 |
136 | tasks.assemble {
137 | dependsOn(renameDmg)
138 | }
139 |
--------------------------------------------------------------------------------
/compose-desktop.pro:
--------------------------------------------------------------------------------
1 | -dontoptimize
2 |
3 | -dontwarn androidx.compose.desktop.DesktopTheme*
4 | -dontwarn kotlinx.datetime.**
5 |
6 | -keep class dev.romainguy.kotlin.explorer.code.*TokenMarker { *; }
7 | -dontnote dev.romainguy.kotlin.explorer.code.*TokenMarker
8 |
9 | -keep class org.fife.** { *; }
10 | -dontnote org.fife.**
11 |
12 | -keep class sun.misc.Unsafe { *; }
13 | -dontnote sun.misc.Unsafe
14 |
15 | -keep class com.jetbrains.JBR* { *; }
16 | -dontnote com.jetbrains.JBR*
17 |
18 | -keep class com.sun.jna** { *; }
19 | -dontnote com.sun.jna**
20 |
21 | -keep class androidx.compose.ui.input.key.KeyEvent_desktopKt { *; }
22 | -dontnote androidx.compose.ui.input.key.KeyEvent_desktopKt
23 |
24 | -keep class androidx.compose.ui.input.key.KeyEvent_skikoKt { *; }
25 | -dontnote androidx.compose.ui.input.key.KeyEvent_skikoKt
26 | -dontwarn androidx.compose.ui.input.key.KeyEvent_skikoKt
27 |
28 | -dontnote org.jetbrains.jewel.intui.markdown.standalone.styling.extensions.**
29 | -dontwarn org.jetbrains.jewel.intui.markdown.standalone.styling.extensions.**
30 |
31 | -dontnote org.jetbrains.jewel.foundation.lazy.**
32 | -dontwarn org.jetbrains.jewel.foundation.lazy.**
33 |
34 | -dontnote org.jetbrains.jewel.foundation.util.**
35 | -dontwarn org.jetbrains.jewel.foundation.util.**
36 |
37 | -dontnote org.jetbrains.jewel.window.utils.**
38 | -dontwarn org.jetbrains.jewel.window.utils.**
39 |
--------------------------------------------------------------------------------
/compose-stability.config:
--------------------------------------------------------------------------------
1 | androidx.collection.*
2 |
3 | java.nio.file.Path
4 |
5 | kotlin.collections.*
6 |
7 | kotlinx.coroutines.CoroutineScope
8 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | kotlin.code.style=official
2 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | collection = "1.5.0"
3 | compose = "1.8.0"
4 | jewel = "1.0.0-SNAPSHOT"
5 | jna = "5.17.0"
6 | junit4 = "4.13.2"
7 | kotlin = "2.1.20"
8 | lifecycle = "2.8.4"
9 | rstaui = "3.3.1"
10 | rsyntaxtextarea="3.4.0"
11 | skiko="0.9.16"
12 | truth = "1.4.2"
13 |
14 | [libraries]
15 | collection = { group = "androidx.collection", name = "collection", version.ref = "collection" }
16 | compose-material3 = { group = "org.jetbrains.compose.material3", name = "material3-desktop", version.ref = "compose" }
17 | compose-splitpane = { group = "org.jetbrains.compose.components", name = "components-splitpane-desktop", version.ref = "compose" }
18 | compose-backhandler = { group = "org.jetbrains.compose.ui", name = "ui-backhandler", version.ref = "compose" }
19 | jewel = { group = "org.jetbrains.jewel", name = "jewel-int-ui-standalone-243", version.ref = "jewel" }
20 | jewel-decorated = { group = "org.jetbrains.jewel", name = "jewel-int-ui-decorated-window-243", version.ref = "jewel" }
21 | jewel-markdown-core = { group = "org.jetbrains.jewel", name = "jewel-markdown-core-243", version.ref = "jewel" }
22 | jewel-markdown-intUiStandaloneStyling = { group = "org.jetbrains.jewel", name = "jewel-markdown-int-ui-standalone-styling-243", version.ref = "jewel" }
23 | jna = { group = "net.java.dev.jna", name = "jna", version.ref = "jna" }
24 | junit4 = { group = "junit", name = "junit", version.ref = "junit4" }
25 | kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlin" }
26 | lifecycle = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime", version.ref = "lifecycle" }
27 | lifecycle-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" }
28 | lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "lifecycle" }
29 | lifecycle-viewmodel-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" }
30 | rstaui = { group = "com.fifesoft", name = "rstaui", version.ref = "rstaui" }
31 | rsyntaxtextarea = { group = "com.fifesoft", name = "rsyntaxtextarea", version.ref = "rsyntaxtextarea" }
32 | skiko-linux = { group = "org.jetbrains.skiko", name = "skiko-awt-runtime-linux-x64", version.ref = "skiko" }
33 | skiko-mac = { group = "org.jetbrains.skiko", name = "skiko-awt-runtime-macos-arm64", version.ref = "skiko" }
34 | truth = { group = "com.google.truth", name = "truth", version.ref = "truth" }
35 |
36 | [plugins]
37 | jetbrains-compose = { id = "org.jetbrains.compose", version.ref = "compose" }
38 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
39 | kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
40 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romainguy/kotlin-explorer/d783c4acc16ac82655c41760568f3be6181db9c4/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
84 |
85 | APP_NAME="Gradle"
86 | APP_BASE_NAME=${0##*/}
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | MAX_FD=$( ulimit -H -n ) ||
147 | warn "Could not query maximum file descriptor limit"
148 | esac
149 | case $MAX_FD in #(
150 | '' | soft) :;; #(
151 | *)
152 | ulimit -n "$MAX_FD" ||
153 | warn "Could not set maximum file descriptor limit to $MAX_FD"
154 | esac
155 | fi
156 |
157 | # Collect all arguments for the java command, stacking in reverse order:
158 | # * args from the command line
159 | # * the main class name
160 | # * -classpath
161 | # * -D...appname settings
162 | # * --module-path (only if needed)
163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
164 |
165 | # For Cygwin or MSYS, switch paths to Windows format before running java
166 | if "$cygwin" || "$msys" ; then
167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
169 |
170 | JAVACMD=$( cygpath --unix "$JAVACMD" )
171 |
172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
173 | for arg do
174 | if
175 | case $arg in #(
176 | -*) false ;; # don't mess with options #(
177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
178 | [ -e "$t" ] ;; #(
179 | *) false ;;
180 | esac
181 | then
182 | arg=$( cygpath --path --ignore --mixed "$arg" )
183 | fi
184 | # Roll the args list around exactly as many times as the number of
185 | # args, so each arg winds up back in the position where it started, but
186 | # possibly modified.
187 | #
188 | # NB: a `for` loop captures its iteration list before it begins, so
189 | # changing the positional parameters here affects neither the number of
190 | # iterations, nor the values presented in `arg`.
191 | shift # remove old arg
192 | set -- "$@" "$arg" # push replacement arg
193 | done
194 | fi
195 |
196 | # Collect all arguments for the java command;
197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
198 | # shell script including quotes and variable substitutions, so put them in
199 | # double quotes to make sure that they get re-expanded; and
200 | # * put everything else in single quotes, so that it's not re-expanded.
201 |
202 | set -- \
203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
204 | -classpath "$CLASSPATH" \
205 | org.gradle.wrapper.GradleWrapperMain \
206 | "$@"
207 |
208 | # Use "xargs" to parse quoted args.
209 | #
210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
211 | #
212 | # In Bash we could simply go:
213 | #
214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
215 | # set -- "${ARGS[@]}" "$@"
216 | #
217 | # but POSIX shell has neither arrays nor command substitution, so instead we
218 | # post-process each arg (as a line of input to sed) to backslash-escape any
219 | # character that might be a shell metacharacter, then use eval to reverse
220 | # that process (while maintaining the separation between arguments), and wrap
221 | # the whole thing up as a single "set" statement.
222 | #
223 | # This will of course break if any of these variables contains a newline or
224 | # an unmatched quote.
225 | #
226 |
227 | eval "set -- $(
228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
229 | xargs -n1 |
230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
231 | tr '\n' ' '
232 | )" '"$@"'
233 |
234 | exec "$JAVACMD" "$@"
235 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/scripts/.gitignore:
--------------------------------------------------------------------------------
1 | aarch64-docs/
2 | bs4-env/
3 | *.kt
4 |
--------------------------------------------------------------------------------
/scripts/aarch64.json:
--------------------------------------------------------------------------------
1 | {
2 | "archive": {
3 | "url": "https://developer.arm.com/-/media/developer/products/architecture/armv9-a-architecture/2023-03/ISA_A64_xml_A_profile-2023-03.tar.gz",
4 | "name": "ISA_A64_xml_A_profile-2023-03.tar.gz",
5 | "subdir": "ISA_A64_xml_A_profile-2023-03"
6 | },
7 | "documentation": "https://developer.arm.com/documentation/ddi0602/2024-06/Base-Instructions/",
8 | "isa": "aarch64"
9 | }
10 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | gradlePluginPortal()
5 | mavenCentral()
6 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
7 | }
8 | }
9 |
10 | plugins {
11 | // Ensure JBR vendor is configured on CI, see https://github.com/actions/setup-java/issues/399.
12 | id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
13 | }
14 |
15 | rootProject.name = "kotlin-explorer"
16 |
17 | include("tests")
18 | include("token-makers")
19 |
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/DependencyCache.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2025 Romain Guy
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 | package dev.romainguy.kotlin.explorer
17 |
18 | import java.io.FileNotFoundException
19 | import java.net.URL
20 | import java.nio.file.Path
21 | import java.nio.file.StandardOpenOption.*
22 | import java.util.zip.ZipInputStream
23 | import kotlin.io.path.createParentDirectories
24 | import kotlin.io.path.notExists
25 | import kotlin.io.path.outputStream
26 |
27 | private const val MAVEN_REPO = "https://repo1.maven.org/maven2"
28 | private const val GOOGLE_REPO = "https://maven.google.com"
29 | private val repos = listOf(MAVEN_REPO, GOOGLE_REPO)
30 |
31 | private const val BASE_PATH = "%1\$s/%2\$s/%3\$s/%2\$s-%3\$s"
32 |
33 | class DependencyCache(private val root: Path) {
34 |
35 | fun getDependency(group: String, name: String, version: String, onOutput: (String) -> Unit): Path {
36 | val basePath = BASE_PATH.format(group.replace('.', '/'), name, version)
37 | val dst = root.resolve("$basePath.jar")
38 | if (dst.notExists()) {
39 | onOutput("Downloading artifact $group:$name:$version")
40 | repos.forEach {
41 | if (getDependency(it, basePath, dst, onOutput)) {
42 | return dst
43 | }
44 | }
45 | onOutput("Could not find artifact $group:$name:$version")
46 | }
47 | return dst
48 | }
49 |
50 | /**
51 | * Try to download a jar from a repo.
52 | *
53 | * First tries to download the `jar` file directly. If the `jar` is not found, will try to download an `aar` and
54 | * extract the `classes.ja` file from it.
55 | *
56 | * @return true if the repo owns the artifact.
57 | */
58 | private fun getDependency(repo: String, basePath: String, dst: Path, onOutput: (String) -> Unit): Boolean {
59 | try {
60 | // Does the artifact exist in repo?
61 | URL("$repo/$basePath.pom").openStream().reader().close()
62 | } catch (_: FileNotFoundException) {
63 | return false
64 | }
65 | dst.createParentDirectories()
66 | dst.outputStream(CREATE, WRITE, TRUNCATE_EXISTING).use { outputStream ->
67 | try {
68 | URL("$repo/$basePath.jar").openStream().use {
69 | it.copyTo(outputStream)
70 | }
71 | } catch (_: FileNotFoundException) {
72 | val aar = "$repo/$basePath.aar"
73 |
74 | ZipInputStream(URL(aar).openStream()).use {
75 | while (true) {
76 | val entry = it.nextEntry
77 | if (entry == null) {
78 | onOutput("Could not find 'classes.jar' in $aar")
79 | return true
80 | }
81 | if (entry.name == "classes.jar") {
82 | it.copyTo(outputStream)
83 | break
84 | }
85 | }
86 | }
87 |
88 | }
89 | }
90 | return true
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Disassembly.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Romain Guy
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 dev.romainguy.kotlin.explorer
18 |
19 | import androidx.compose.ui.text.AnnotatedString
20 | import androidx.compose.ui.text.SpanStyle
21 | import androidx.compose.ui.text.buildAnnotatedString
22 | import androidx.compose.ui.text.withStyle
23 | import dev.romainguy.kotlin.explorer.build.ByteCodeDecompiler
24 | import dev.romainguy.kotlin.explorer.build.DexCompiler
25 | import dev.romainguy.kotlin.explorer.build.KotlinCompiler
26 | import dev.romainguy.kotlin.explorer.bytecode.ByteCodeParser
27 | import dev.romainguy.kotlin.explorer.code.CodeContent
28 | import dev.romainguy.kotlin.explorer.code.ISA
29 | import dev.romainguy.kotlin.explorer.dex.DexDumpParser
30 | import dev.romainguy.kotlin.explorer.oat.OatDumpParser
31 | import kotlinx.coroutines.*
32 | import java.io.File
33 | import java.nio.file.Files
34 | import java.nio.file.Path
35 | import kotlin.io.path.extension
36 | import kotlin.io.path.isDirectory
37 |
38 | private const val TotalDisassemblySteps = 7
39 | private const val TotalRunSteps = 2
40 |
41 | private val byteCodeDecompiler = ByteCodeDecompiler()
42 | private val byteCodeParser = ByteCodeParser()
43 | private val dexDumpParser = DexDumpParser()
44 | private val oatDumpParser = OatDumpParser()
45 |
46 | suspend fun buildAndRun(
47 | toolPaths: ToolPaths,
48 | kotlinOnlyConsumers: Boolean,
49 | compilerFlags: String,
50 | settingsDirectory: Path,
51 | composeVersion: String,
52 | source: String,
53 | onLogs: (AnnotatedString) -> Unit,
54 | onStatusUpdate: (String, Float) -> Unit
55 | ) = coroutineScope {
56 | val ui = currentCoroutineContext()
57 |
58 | launch(Dispatchers.IO) {
59 | val updater = ProgressUpdater(TotalRunSteps, onStatusUpdate)
60 | try {
61 | updater.addJob(launch(ui) { updater.update("Compiling Kotlin…") })
62 |
63 | val directory = toolPaths.tempDirectory
64 | cleanupClasses(directory)
65 |
66 | val path = directory.resolve("KotlinExplorer.kt")
67 | Files.writeString(path, source)
68 | writeSupportFiles(directory)
69 |
70 | val kotlinc = KotlinCompiler(toolPaths, settingsDirectory, directory).compile(kotlinOnlyConsumers, compilerFlags, composeVersion, path)
71 |
72 | if (kotlinc.exitCode != 0) {
73 | withContext(ui) {
74 | onLogs(showError(kotlinc.output.replace(path.parent.toString() + "/", "")))
75 | updater.advance("Error compiling Kotlin", 2)
76 | }
77 | return@launch
78 | }
79 |
80 | withContext(ui) { updater.advance("Running…") }
81 |
82 | val java = process(
83 | *buildJavaCommand(toolPaths),
84 | directory = directory
85 | )
86 |
87 | withContext(ui) {
88 | onLogs(showLogs(java.output))
89 | val status = if (java.exitCode != 0) "Error running code" else "Run completed"
90 | updater.advance(status)
91 | }
92 |
93 | updater.addJob(launch(ui) { updater.update("Ready") })
94 | } finally {
95 | withContext(ui) { updater.finish() }
96 | }
97 | }
98 | }
99 |
100 | suspend fun buildAndDisassemble(
101 | toolPaths: ToolPaths,
102 | source: String,
103 | kotlinOnlyConsumers: Boolean,
104 | compilerFlags: String,
105 | r8rules: String,
106 | minApi: Int,
107 | settingsDirectory: Path,
108 | composeVersion: String,
109 | instructionSets: Map,
110 | onByteCode: (CodeContent) -> Unit,
111 | onDex: (CodeContent) -> Unit,
112 | onOat: (CodeContent) -> Unit,
113 | onLogs: (AnnotatedString) -> Unit,
114 | onStatusUpdate: (String, Float) -> Unit,
115 | optimize: Boolean,
116 | keepEverything: Boolean
117 | ) = coroutineScope {
118 | val ui = currentCoroutineContext()
119 |
120 | onLogs(showLogs(""))
121 |
122 | launch(Dispatchers.IO) {
123 | val updater = ProgressUpdater(TotalDisassemblySteps, onStatusUpdate)
124 | try {
125 | updater.addJob(launch(ui) { updater.update("Compiling and disassembling…") })
126 |
127 | val directory = toolPaths.tempDirectory
128 | cleanupClasses(directory)
129 |
130 | val path = directory.resolve("KotlinExplorer.kt")
131 | Files.writeString(path, source)
132 | writeSupportFiles(directory)
133 |
134 | val kotlinc = KotlinCompiler(toolPaths, settingsDirectory, directory).compile(kotlinOnlyConsumers, compilerFlags, composeVersion, path)
135 |
136 | if (kotlinc.exitCode != 0) {
137 | updater.addJob(launch(ui) {
138 | onLogs(showError(kotlinc.output.replace(path.parent.toString() + "/", "")))
139 | updater.advance("Error compiling Kotlin", updater.steps)
140 | })
141 | return@launch
142 | }
143 | updater.addJob(launch(ui) { updater.advance("Kotlin compiled") })
144 |
145 | if (instructionSets.getOrDefault(ISA.ByteCode, true)) {
146 | updater.addJob(launch {
147 | val javap = byteCodeDecompiler.decompile(directory)
148 | withContext(ui) {
149 | val status = if (javap.exitCode != 0) {
150 | onLogs(showError(javap.output))
151 | "Error Disassembling Java ByteCode"
152 | } else {
153 | onByteCode(byteCodeParser.parse(javap.output))
154 | "Disassembled Java ByteCode"
155 | }
156 | updater.advance(status)
157 | }
158 | })
159 | } else {
160 | launch(ui) { updater.advance("") }
161 | }
162 |
163 | val dexCompiler = DexCompiler(toolPaths, directory, r8rules, minApi)
164 |
165 | val dex = dexCompiler.buildDex(optimize, keepEverything)
166 |
167 | if (dex.exitCode != 0) {
168 | updater.addJob(launch(ui) {
169 | onLogs(showError(dex.output))
170 | updater.advance("Error creating DEX", updater.steps)
171 | })
172 | return@launch
173 | }
174 | updater.addJob(launch(ui) {
175 | updater.advance(if (optimize) "Optimized DEX with R8" else "Compiled DEX with D8")
176 | })
177 |
178 | if (instructionSets.getOrDefault(ISA.Dex, true)) {
179 | updater.addJob(launch {
180 | val dexdump = dexCompiler.dumpDex()
181 | withContext(ui) {
182 | val status = if (dexdump.exitCode != 0) {
183 | onLogs(showError(dexdump.output))
184 | "Error creating DEX dump"
185 | } else {
186 | onDex(dexDumpParser.parse(dexdump.output))
187 | "Created DEX dump"
188 | }
189 | updater.advance(status)
190 | }
191 | })
192 | } else {
193 | launch(ui) { updater.advance("") }
194 | }
195 |
196 | if (!instructionSets.getOrDefault(ISA.Oat, true)) {
197 | updater.waitForJobs()
198 | withContext(ui) {
199 | updater.skipToEnd("Ready")
200 | }
201 | return@launch
202 | }
203 |
204 | val push = process(
205 | toolPaths.adb.toString(),
206 | "push",
207 | "classes.dex",
208 | "/sdcard/classes.dex",
209 | directory = directory
210 | )
211 |
212 | if (push.exitCode != 0) {
213 | updater.addJob(launch(ui) {
214 | onLogs(showError(push.output))
215 | updater.advance("Error pushing code to device", updater.steps)
216 | })
217 | return@launch
218 | }
219 |
220 | updater.addJob(launch(ui) { updater.advance("Pushed code to device…") })
221 |
222 | val dex2oat = process(
223 | toolPaths.adb.toString(),
224 | "shell",
225 | "dex2oat",
226 | "--dex-file=/sdcard/classes.dex",
227 | "--oat-file=/sdcard/classes.oat",
228 | directory = directory
229 | )
230 |
231 | if (dex2oat.exitCode != 0) {
232 | updater.addJob(launch(ui) {
233 | onLogs(showError(dex2oat.output))
234 | updater.advance("Error compiling OAT", updater.steps)
235 | })
236 | return@launch
237 | }
238 |
239 | updater.addJob(launch(ui) { updater.advance("Disassembling OAT…") })
240 |
241 | val oatdump = process(
242 | toolPaths.adb.toString(),
243 | "shell",
244 | "oatdump",
245 | "--oat-file=/sdcard/classes.oat",
246 | directory = directory
247 | )
248 |
249 | updater.addJob(launch(ui) { onOat(oatDumpParser.parse(oatdump.output)) })
250 |
251 | val status = if (oatdump.exitCode != 0) {
252 | onLogs(showError(oatdump.output))
253 | "Error creating oat dump"
254 | } else {
255 | "Created oat dump"
256 | }
257 | withContext(ui) { updater.advance(status) }
258 |
259 | updater.addJob(launch(ui) { updater.update("Ready") })
260 | } finally {
261 | withContext(ui) { updater.finish() }
262 | }
263 | }
264 | }
265 |
266 | private fun buildJavaCommand(toolPaths: ToolPaths): Array {
267 | val classpath = toolPaths.kotlinLibs.joinToString(File.pathSeparator) { jar -> jar.toString() } +
268 | File.pathSeparator + toolPaths.platform +
269 | File.pathSeparator + "."
270 |
271 | val command = mutableListOf(
272 | "java",
273 | "-classpath",
274 | classpath,
275 | "KotlinExplorerKt"
276 | )
277 | return command.toTypedArray()
278 | }
279 |
280 | private fun cleanupClasses(directory: Path) {
281 | Files
282 | .list(directory)
283 | .forEach { path ->
284 | if (path.extension == "class") {
285 | path.toFile().delete()
286 | } else if (path.isDirectory()) {
287 | path.toFile().deleteRecursively()
288 | }
289 | }
290 | }
291 |
292 | private fun writeSupportFiles(directory: Path) {
293 | Files.writeString(
294 | directory.resolve("NeverInline.kt"),
295 | """
296 | @file:OptIn(ExperimentalMultiplatform::class)
297 |
298 | package dalvik.annotation.optimization
299 |
300 | @Retention(AnnotationRetention.BINARY)
301 | @Target(AnnotationTarget.CONSTRUCTOR, AnnotationTarget.FUNCTION)
302 | public annotation class NeverInline()
303 | """.trimIndent()
304 | )
305 |
306 | Files.writeString(
307 | directory.resolve("Keep.kt"),
308 | """
309 | import java.lang.annotation.ElementType.ANNOTATION_TYPE
310 | import java.lang.annotation.ElementType.CONSTRUCTOR
311 | import java.lang.annotation.ElementType.FIELD
312 | import java.lang.annotation.ElementType.METHOD
313 | import java.lang.annotation.ElementType.PACKAGE
314 | import java.lang.annotation.ElementType.TYPE
315 |
316 | @Retention(AnnotationRetention.BINARY)
317 | @Target(
318 | AnnotationTarget.FILE,
319 | AnnotationTarget.ANNOTATION_CLASS,
320 | AnnotationTarget.CLASS,
321 | AnnotationTarget.ANNOTATION_CLASS,
322 | AnnotationTarget.CONSTRUCTOR,
323 | AnnotationTarget.FUNCTION,
324 | AnnotationTarget.PROPERTY_GETTER,
325 | AnnotationTarget.PROPERTY_SETTER,
326 | AnnotationTarget.FIELD
327 | )
328 | @Suppress("DEPRECATED_JAVA_ANNOTATION", "SupportAnnotationUsage")
329 | @java.lang.annotation.Target(PACKAGE, TYPE, ANNOTATION_TYPE, CONSTRUCTOR, METHOD, FIELD)
330 | public annotation class Keep
331 | """.trimIndent()
332 | )
333 | }
334 |
335 | private fun showError(error: String) = buildAnnotatedString {
336 | withStyle(SpanStyle(ErrorColor)) {
337 | append(error)
338 | }
339 | }
340 |
341 | private fun showLogs(logs: String) = AnnotatedString(logs)
342 |
343 | internal val BuiltInKotlinClass = Regex("^(kotlin|kotlinx|java|javax|org\\.(intellij|jetbrains))\\..+")
344 |
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/DocumentChangeListener.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Romain Guy
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 dev.romainguy.kotlin.explorer
18 |
19 | import javax.swing.event.DocumentEvent
20 | import javax.swing.event.DocumentListener
21 |
22 | /**
23 | * A [DocumentListener] that takes a single lambda that's invoked on any change
24 | */
25 | class DocumentChangeListener(private val block: (DocumentEvent) -> Unit) : DocumentListener {
26 | override fun insertUpdate(event: DocumentEvent) {
27 | block(event)
28 | }
29 |
30 | override fun removeUpdate(event: DocumentEvent) {
31 | block(event)
32 | }
33 |
34 | override fun changedUpdate(event: DocumentEvent) {
35 | block(event)
36 | }
37 | }
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Logger.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Romain Guy
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 dev.romainguy.kotlin.explorer
18 |
19 | private const val Debug = 1
20 | private const val Warning = 2
21 |
22 | object Logger {
23 | private val level = System.getenv("KOTLIN_EXPLORER_LOG")?.toIntOrNull() ?: 0
24 |
25 | fun debug(message: String) {
26 | if (level >= Debug) {
27 | println("Debug: $message")
28 | }
29 | }
30 |
31 | fun warn(message: String) {
32 | if (level >= Warning) {
33 | println("Warning: $message")
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Menu.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2024 Romain Guy
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 | @file:Suppress("FunctionName")
18 |
19 | package dev.romainguy.kotlin.explorer
20 |
21 | import androidx.compose.runtime.Composable
22 | import androidx.compose.ui.input.key.Key
23 | import androidx.compose.ui.input.key.KeyShortcut
24 | import androidx.compose.ui.window.MenuScope
25 | import kotlin.reflect.KMutableProperty0
26 |
27 | /** Convenience class handles `Ctrl <-> Meta` modifies */
28 | sealed class Shortcut(
29 | private val key: Key,
30 | private val isShift: Boolean,
31 | private val isCtrl: Boolean,
32 | private val isAlt: Boolean = false,
33 | private val metaOnMac: Boolean = true
34 | ) {
35 | class Ctrl(key: Key) : Shortcut(key, isCtrl = true, isShift = false)
36 | class CtrlAlt(key: Key) : Shortcut(key, isCtrl = true, isShift = false, isAlt = true)
37 | class CtrlOnly(key: Key) : Shortcut(key, isCtrl = true, isShift = false, metaOnMac = false)
38 | class CtrlShift(key: Key) : Shortcut(key, isCtrl = true, isShift = true)
39 |
40 | fun asKeyShortcut() =
41 | KeyShortcut(
42 | key = key,
43 | ctrl = isCtrl && (!isMac || !metaOnMac),
44 | shift = isShift,
45 | meta = isCtrl && isMac && metaOnMac,
46 | alt = isAlt
47 | )
48 | }
49 |
50 | @Composable
51 | fun MenuScope.MenuCheckboxItem(
52 | text: String,
53 | shortcut: Shortcut?,
54 | property: KMutableProperty0,
55 | onCheckedChanged: (Boolean) -> Unit = {}
56 | ) {
57 | CheckboxItem(text, property.get(), shortcut = shortcut?.asKeyShortcut(), onCheckedChange = {
58 | property.set(it)
59 | onCheckedChanged(it)
60 | })
61 | }
62 |
63 | @Composable
64 | fun MenuScope.MenuItem(text: String, shortcut: Shortcut, onClick: () -> Unit, enabled: Boolean = true) {
65 | Item(text, enabled = enabled, shortcut = shortcut.asKeyShortcut(), onClick = onClick)
66 | }
67 |
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/OS.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2024 Romain Guy
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 dev.romainguy.kotlin.explorer
18 |
19 | val isWindows: Boolean = System.getProperty("os.name").lowercase().startsWith("win")
20 | val isMac: Boolean = System.getProperty("os.name").lowercase().startsWith("mac")
21 | val isLinux = !isWindows && !isMac
22 |
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Paths.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Romain Guy
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 dev.romainguy.kotlin.explorer
18 |
19 | import java.nio.file.Files
20 | import java.nio.file.Path
21 | import kotlin.io.path.*
22 |
23 | class ToolPaths(settingsDirectory: Path, androidHome: Path, kotlinHome: Path) {
24 | constructor(settingsDirectory: Path, androidHome: String, kotlinHome: String) : this(
25 | settingsDirectory,
26 | Path.of(androidHome),
27 | Path.of(kotlinHome)
28 | )
29 | val tempDirectory = Files.createTempDirectory("kotlin-explorer")!!
30 | val platform: Path
31 | val d8: Path
32 | val adb: Path = androidHome.resolve(if (isWindows) "platform-tools/adb.exe" else "platform-tools/adb")
33 | val dexdump: Path
34 | val kotlinc: Path
35 | val kotlinLibs: List
36 | val sourceFile: Path
37 |
38 | var isValid: Boolean = false
39 | private set
40 | var isAndroidHomeValid: Boolean = false
41 | private set
42 | var isKotlinHomeValid: Boolean = false
43 | private set
44 |
45 | init {
46 | val buildToolsDirectory = androidHome.resolve("build-tools")
47 | .listIfExists()
48 | .maxByOrNull { it.pathString }
49 | ?: androidHome
50 | d8 = System.getenv("D8_PATH")?.toPath() ?: buildToolsDirectory.resolve("lib/d8.jar")
51 | dexdump = buildToolsDirectory.resolve(if (isWindows) "dexdump.exe" else "dexdump")
52 |
53 | val platformsDirectory = androidHome.resolve("platforms")
54 | .listIfExists()
55 | .maxByOrNull { it.pathString }
56 | ?: androidHome
57 | platform = platformsDirectory.resolve("android.jar")
58 |
59 | kotlinc = kotlinHome.resolve(if (isWindows) "bin/kotlinc.bat" else "bin/kotlinc")
60 |
61 | val lib = kotlinHome.resolveFirstExistsOrFirst("lib", "libexec/lib")
62 | kotlinLibs = listOf(
63 | lib.resolve("kotlin-stdlib-jdk8.jar"),
64 | lib.resolve("kotlin-stdlib.jar"),
65 | lib.resolve("kotlin-annotations-jvm.jar"),
66 | lib.resolve("kotlinx-coroutines-core-jvm.jar"),
67 | lib.listIfExists()
68 | .filter { path -> path.extension == "jar" && path.name.startsWith("annotations-") }
69 | .maxByOrNull { it.pathString }
70 | ?: lib.resolve("annotations.jar")
71 | )
72 |
73 | sourceFile = settingsDirectory.resolve("source-code.kt")
74 |
75 | isAndroidHomeValid = adb.exists() && d8.exists() && dexdump.exists()
76 | isKotlinHomeValid = kotlinc.exists()
77 | isValid = adb.exists() && d8.exists() && dexdump.exists() && kotlinc.exists()
78 | }
79 | }
80 |
81 | private fun Path.listIfExists() = if (exists()) listDirectoryEntries() else emptyList()
82 |
83 | private fun String.toPath() = Path.of(this)
84 |
85 | private fun Path.resolveFirstExistsOrFirst(vararg others: String): Path {
86 | for (other in others) {
87 | val path = resolve(other)
88 | if (path.exists()) {
89 | return path
90 | }
91 | }
92 |
93 | return resolve(others.first())
94 | }
95 |
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/PeekingIterator.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2024 Romain Guy
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 dev.romainguy.kotlin.explorer
18 |
19 | /** Based on Guava PeekingIterator */
20 | class PeekingIterator(private val iterator: Iterator) : Iterator {
21 | private var peekedElement: E? = null
22 |
23 | override fun hasNext(): Boolean {
24 | return peekedElement != null || iterator.hasNext()
25 | }
26 |
27 | override fun next(): E {
28 | val element = peekedElement ?: return iterator.next()
29 | peekedElement = null
30 | return element
31 | }
32 |
33 | fun peek(): E {
34 | return peekedElement.takeIf { it != null } ?: iterator.next().also { peekedElement = it }
35 | }
36 | }
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Process.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Romain Guy
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 dev.romainguy.kotlin.explorer
18 |
19 | import kotlinx.coroutines.*
20 | import kotlinx.coroutines.flow.asFlow
21 | import kotlinx.coroutines.flow.map
22 | import kotlinx.coroutines.flow.toList
23 | import java.nio.file.Path
24 |
25 | class ProcessResult(
26 | val exitCode: Int,
27 | val output: String
28 | )
29 |
30 | suspend fun process(
31 | vararg command: String,
32 | directory: Path? = null
33 | ): ProcessResult {
34 | return withContext(Dispatchers.IO) {
35 | val process = ProcessBuilder()
36 | .directory(directory?.toFile())
37 | .command(*command)
38 | .redirectErrorStream(true)
39 | .start()
40 |
41 | val output = async {
42 | process
43 | .inputStream
44 | .bufferedReader()
45 | .lineSequence()
46 | .asFlow()
47 | .map { value ->
48 | yield()
49 | value
50 | }
51 | .toList()
52 | .joinToString(System.lineSeparator())
53 | }
54 |
55 | try {
56 | val exitCode = runInterruptible {
57 | process.waitFor()
58 | }
59 | val processOutput = output.await()
60 | ProcessResult(exitCode, processOutput)
61 | } catch (e: CancellationException) {
62 | throw e
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/ProgressUpdater.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Romain Guy
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 dev.romainguy.kotlin.explorer
18 |
19 | import kotlinx.coroutines.Job
20 | import kotlinx.coroutines.joinAll
21 | import java.util.concurrent.atomic.AtomicInteger
22 | import java.util.concurrent.locks.ReentrantReadWriteLock
23 | import kotlin.concurrent.read
24 | import kotlin.concurrent.write
25 | import kotlin.math.min
26 |
27 | /** Helps manage updates to a status bar */
28 | class ProgressUpdater(
29 | val steps: Int,
30 | private val onUpdate: (String, Float) -> Unit,
31 | ) {
32 | private val stepCounter = AtomicInteger(0)
33 | private val jobs = mutableListOf()
34 |
35 | /** Update without advancing progress */
36 | fun update(message: String) {
37 | sendUpdate(message, stepCounter.get())
38 | }
39 |
40 | /**
41 | * Update and advance progress
42 | *
43 | * We can advance by more than one step, for example, if a step fails and that prevents the next 3 steps from being
44 | * able to run, we would advance by 4.
45 | */
46 | fun advance(message: String, steps: Int = 1) {
47 | sendUpdate(message, stepCounter.addAndGet(steps))
48 | }
49 |
50 | suspend fun waitForJobs() {
51 | jobs.joinAll()
52 | }
53 |
54 | fun skipToEnd(message: String) {
55 | stepCounter.set(steps)
56 | sendUpdate(message, steps)
57 | }
58 |
59 | /**
60 | * Joins all threads and sends the last update
61 | */
62 | suspend fun finish() {
63 | jobs.joinAll()
64 | val step = stepCounter.get()
65 | if (step < steps) {
66 | Logger.warn("finish() called but progress is not yet finished: step=$step")
67 | }
68 | }
69 |
70 | /** Add a job that needs to be joined before finishing */
71 | fun addJob(job: Job) {
72 | jobs.add(job)
73 | }
74 |
75 | private fun sendUpdate(message: String, step: Int) {
76 | if (step > steps) {
77 | Logger.warn("Progress already completed while sending: '$message'")
78 | }
79 | Logger.debug("Sending $step/$steps: '$message'")
80 | onUpdate(message, min(steps, step).toFloat() / steps)
81 | }
82 | }
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Regex.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Romain Guy
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 dev.romainguy.kotlin.explorer
18 |
19 | const val HexDigit = "[0-9a-fA-F]"
20 |
21 | fun MatchResult.getValue(group: String): String {
22 | return groups[group]?.value ?: throw IllegalStateException("Value of $group not found in $value")
23 | }
24 |
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Settings.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Romain Guy
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 | @file:Suppress("FunctionName")
18 |
19 | package dev.romainguy.kotlin.explorer
20 |
21 | import androidx.compose.foundation.ExperimentalFoundationApi
22 | import androidx.compose.foundation.layout.*
23 | import androidx.compose.foundation.onClick
24 | import androidx.compose.foundation.text.input.InputTransformation
25 | import androidx.compose.foundation.text.input.TextFieldLineLimits
26 | import androidx.compose.foundation.text.input.TextFieldState
27 | import androidx.compose.foundation.text.input.rememberTextFieldState
28 | import androidx.compose.runtime.*
29 | import androidx.compose.ui.Alignment
30 | import androidx.compose.ui.Modifier
31 | import androidx.compose.ui.unit.dp
32 | import org.jetbrains.jewel.ui.component.*
33 | import org.jetbrains.jewel.ui.icon.PathIconKey
34 |
35 |
36 | @Composable
37 | fun Settings(
38 | state: ExplorerState,
39 | onSaveRequest: () -> Unit,
40 | onDismissRequest: () -> Unit
41 | ) {
42 | val androidHome = rememberTextFieldState(state.androidHome)
43 | val kotlinHome = rememberTextFieldState(state.kotlinHome)
44 | val kotlinOnlyConsumers = remember { mutableStateOf(state.kotlinOnlyConsumers) }
45 | val compilerFlags = rememberTextFieldState(state.compilerFlags)
46 | val r8rules = rememberTextFieldState(state.r8Rules)
47 | val composeVersion = rememberTextFieldState(state.composeVersion)
48 | val minApi = rememberTextFieldState(state.minApi.toString())
49 | val indent = rememberTextFieldState(state.indent.toString())
50 | val lineNumberWidth = rememberTextFieldState(state.lineNumberWidth.toString())
51 | val decompileHiddenIsa = remember { mutableStateOf(state.decompileHiddenIsa) }
52 | val onSaveClick = {
53 | state.saveState(
54 | androidHome.text.toString(),
55 | kotlinHome.text.toString(),
56 | kotlinOnlyConsumers.value,
57 | compilerFlags.text.toString(),
58 | r8rules.text.toString(),
59 | composeVersion.text.toString(),
60 | minApi.text.toString(),
61 | indent.text.toString(),
62 | lineNumberWidth.text.toString(),
63 | decompileHiddenIsa.value
64 | )
65 | onSaveRequest()
66 | }
67 |
68 | Column(verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(16.dp)) {
69 | val toolPaths = ToolPaths(state.directory, androidHome.text.toString(), kotlinHome.text.toString())
70 | StringSetting("Android home directory: ", androidHome) { toolPaths.isAndroidHomeValid }
71 | StringSetting("Kotlin home directory: ", kotlinHome) { toolPaths.isKotlinHomeValid }
72 | IntSetting("Decompiled code indent: ", indent, minValue = 2)
73 | IntSetting("Line number column width: ", lineNumberWidth, minValue = 1)
74 | StringSetting("Kotlin compiler flags: ", compilerFlags)
75 | MultiLineStringSetting("R8 rules: ", r8rules)
76 | StringSetting("Compose version: ", composeVersion) {composeVersion.text.isNotEmpty()}
77 | IntSetting("Min API: ", minApi, minValue = 1)
78 | BooleanSetting("Kotlin only consumers", kotlinOnlyConsumers)
79 | BooleanSetting("Decompile hidden instruction sets", decompileHiddenIsa)
80 | Spacer(modifier = Modifier.height(8.dp))
81 | Buttons(saveEnabled = toolPaths.isValid, onSaveClick, onDismissRequest)
82 | }
83 | }
84 |
85 | @Composable
86 | private fun ColumnScope.Buttons(
87 | saveEnabled: Boolean,
88 | onSaveClick: () -> Unit,
89 | onCancelClick: () -> Unit
90 | ) {
91 | Row(horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.align(Alignment.End)) {
92 | DefaultButton(onClick = onCancelClick) {
93 | Text("Cancel")
94 | }
95 | DefaultButton(enabled = saveEnabled, onClick = onSaveClick) {
96 | Text("Save")
97 | }
98 | }
99 | }
100 |
101 | private fun ExplorerState.saveState(
102 | androidHome: String,
103 | kotlinHome: String,
104 | kotlinOnlyConsumers: Boolean,
105 | compilerFlags: String,
106 | r8Rules: String,
107 | composeVersion: String,
108 | minApi: String,
109 | indent: String,
110 | lineNumberWidth: String,
111 | decompileHiddenIsa: Boolean,
112 | ) {
113 | this.androidHome = androidHome
114 | this.kotlinHome = kotlinHome
115 | this.kotlinOnlyConsumers = kotlinOnlyConsumers
116 | this.compilerFlags = compilerFlags
117 | this.r8Rules = r8Rules
118 | this.composeVersion = composeVersion
119 | this.minApi = minApi.toIntOrNull() ?: 21
120 | this.indent = indent.toIntOrNull() ?: 4
121 | this.lineNumberWidth = lineNumberWidth.toIntOrNull() ?: 4
122 | this.decompileHiddenIsa = decompileHiddenIsa
123 | this.reloadToolPathsFromSettings()
124 | }
125 |
126 | @Composable
127 | private fun StringSetting(title: String, state: TextFieldState, isValid: () -> Boolean = { true }) {
128 | SettingRow(title, state, isValid)
129 | }
130 |
131 | @Composable
132 | private fun ColumnScope.MultiLineStringSetting(title: String, state: TextFieldState) {
133 | Row(Modifier.weight(1.0f)) {
134 | Text(
135 | title,
136 | modifier = Modifier
137 | .alignByBaseline()
138 | .defaultMinSize(minWidth = 200.dp),
139 | )
140 | TextArea(
141 | state,
142 | modifier = Modifier
143 | .weight(1.0f)
144 | .fillMaxHeight(),
145 | lineLimits = TextFieldLineLimits.MultiLine(10, 10)
146 | )
147 | }
148 | }
149 |
150 | @Composable
151 | private fun IntSetting(title: String, state: TextFieldState, minValue: Int) {
152 | val isValid by derivedStateOf {
153 | (state.text.toString().toIntOrNull() ?: Int.MIN_VALUE) >= minValue
154 | }
155 |
156 | SettingRow(title, state, isValid = { isValid }, {
157 | val changed = this.toString()
158 | if (changed.isNotEmpty() && changed.toIntOrNull() == null) {
159 | revertAllChanges()
160 | }
161 | })
162 | }
163 |
164 | @OptIn(ExperimentalFoundationApi::class)
165 | @Composable
166 | private fun BooleanSetting(@Suppress("SameParameterValue") title: String, state: MutableState) {
167 | Row {
168 | Checkbox(state.value, onCheckedChange = { state.value = it })
169 | Text(
170 | title,
171 | modifier = Modifier
172 | .align(Alignment.CenterVertically)
173 | .padding(start = 2.dp)
174 | .onClick {
175 | state.value = !state.value
176 | }
177 | )
178 | }
179 | }
180 |
181 | @Composable
182 | private fun SettingRow(
183 | title: String,
184 | state: TextFieldState,
185 | isValid: () -> Boolean,
186 | inputTransformation: InputTransformation? = null
187 | ) {
188 | Row(Modifier.fillMaxWidth()) {
189 | Text(
190 | title,
191 | modifier = Modifier
192 | .alignByBaseline()
193 | .defaultMinSize(minWidth = 200.dp),
194 | )
195 | TextField(
196 | state,
197 | modifier = Modifier
198 | .alignByBaseline()
199 | .defaultMinSize(minWidth = 360.dp)
200 | .weight(1.0f),
201 | inputTransformation = inputTransformation,
202 | trailingIcon = { if (isValid()) ValidIcon() else ErrorIcon() }
203 | )
204 | }
205 | }
206 |
207 | @Composable
208 | private fun ErrorIcon() {
209 | Icon(
210 | key = PathIconKey(
211 | "icons/error.svg",
212 | iconClass = ExplorerState::class.java
213 | ), contentDescription = "Error",
214 | tint = IconErrorColor,
215 | hints = arrayOf()
216 | )
217 | }
218 |
219 | @Composable
220 | private fun ValidIcon() {
221 | Icon(
222 | key = PathIconKey(
223 | "icons/done.svg",
224 | iconClass = ExplorerState::class.java
225 | ), contentDescription = "Valid",
226 | tint = IconValidColor,
227 | hints = arrayOf()
228 | )
229 | }
230 |
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/SourceTextArea.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2024 The Android Open Source Project
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 dev.romainguy.kotlin.explorer
18 |
19 | import dev.romainguy.kotlin.explorer.code.CodeTextArea
20 | import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea
21 | import java.awt.event.MouseAdapter
22 | import java.awt.event.MouseEvent
23 |
24 | class SourceTextArea(
25 | var isSyncLinesEnabled: Boolean
26 | ) : SyntaxTextArea() {
27 | private val codeTextAreas = mutableListOf()
28 |
29 | init {
30 | addMouseListener(object : MouseAdapter() {
31 | override fun mouseClicked(event: MouseEvent) {
32 | if (isSyncLinesEnabled) {
33 | codeTextAreas.forEach {
34 | it.gotoSourceLine(getLineOfOffset(viewToModel2D(event.point)))
35 | }
36 | }
37 | }
38 | })
39 | }
40 |
41 | fun addCodeTextAreas(vararg codeTextAreas: CodeTextArea) {
42 | this.codeTextAreas.addAll(codeTextAreas)
43 | }
44 |
45 | fun gotoLine(src: CodeTextArea, line: Int) {
46 | caretPosition = getLineStartOffset(line.coerceIn(0 until lineCount))
47 | centerCaretInView()
48 | // Sync other `CodeTextArea` to same line as the `src` sent
49 | codeTextAreas.filter { it !== src }.forEach { it.gotoSourceLine(line) }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Splitter.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Romain Guy
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 | @file:Suppress("FunctionName", "OPT_IN_USAGE", "KotlinRedundantDiagnosticSuppress")
18 | @file:OptIn(ExperimentalSplitPaneApi::class)
19 |
20 | package dev.romainguy.kotlin.explorer
21 |
22 | import androidx.compose.foundation.layout.*
23 | import androidx.compose.runtime.Composable
24 | import androidx.compose.ui.Modifier
25 | import androidx.compose.ui.input.pointer.PointerIcon
26 | import androidx.compose.ui.input.pointer.pointerHoverIcon
27 | import androidx.compose.ui.unit.dp
28 | import org.jetbrains.compose.splitpane.*
29 | import java.awt.Cursor
30 |
31 | private fun Modifier.cursorForHorizontalResize(): Modifier =
32 | pointerHoverIcon(PointerIcon(Cursor(Cursor.E_RESIZE_CURSOR)))
33 |
34 | private fun Modifier.cursorForVerticalResize(): Modifier =
35 | pointerHoverIcon(PointerIcon(Cursor(Cursor.N_RESIZE_CURSOR)))
36 |
37 | fun SplitterScope.HorizontalSplitter() {
38 | visiblePart {
39 | Box(
40 | Modifier
41 | .width(5.dp)
42 | .fillMaxHeight()
43 | )
44 | }
45 | handle {
46 | Box(
47 | Modifier
48 | .markAsHandle()
49 | .cursorForHorizontalResize()
50 | .width(5.dp)
51 | .fillMaxHeight()
52 | )
53 | }
54 | }
55 |
56 | fun SplitterScope.VerticalSplitter() {
57 | visiblePart {
58 | Box(
59 | Modifier
60 | .height(5.dp)
61 | .fillMaxWidth()
62 | )
63 | }
64 | handle {
65 | Box(
66 | Modifier
67 | .markAsHandle()
68 | .cursorForVerticalResize()
69 | .height(5.dp)
70 | .fillMaxWidth()
71 | )
72 | }
73 | }
74 |
75 | @Composable
76 | fun MultiSplitter(modifier: Modifier = Modifier, panels: List<@Composable () -> Unit>) {
77 | val size = panels.size
78 | if (size == 1) {
79 | panels[0]()
80 | } else {
81 | HorizontalSplitPane(
82 | modifier = modifier,
83 | splitPaneState = rememberSplitPaneState(initialPositionPercentage = 1.0f / size)
84 | ) {
85 | first { panels[0]() }
86 | second { MultiSplitter(modifier = modifier, panels.drop(1)) }
87 | splitter { HorizontalSplitter() }
88 | }
89 | }
90 | }
91 |
92 | @Composable
93 | fun VerticalOptionalPanel(
94 | modifier: Modifier = Modifier,
95 | showOptionalPanel: Boolean = false,
96 | optionalPanel: @Composable () -> Unit,
97 | content: @Composable () -> Unit
98 | ) {
99 | if (showOptionalPanel) {
100 | VerticalSplitPane(
101 | modifier = modifier,
102 | splitPaneState = rememberSplitPaneState(initialPositionPercentage = 4f / 5f)
103 | ) {
104 | first { content() }
105 | second { optionalPanel() }
106 | splitter { VerticalSplitter() }
107 | }
108 | } else {
109 | content()
110 | }
111 | }
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/State.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Romain Guy
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 dev.romainguy.kotlin.explorer
18 |
19 | import androidx.compose.runtime.*
20 | import androidx.compose.ui.window.WindowPlacement
21 | import androidx.compose.ui.window.WindowPlacement.Floating
22 | import java.nio.file.Files
23 | import java.nio.file.Path
24 | import java.nio.file.Paths
25 | import kotlin.io.path.exists
26 | import kotlin.io.path.readLines
27 |
28 | private const val AndroidHome = "ANDROID_HOME"
29 | private const val KotlinHome = "KOTLIN_HOME"
30 | private const val Optimize = "OPTIMIZE"
31 | private const val KeepEverything = "KEEP_EVERYTHING"
32 | private const val KotlinOnlyConsumers = "KOTLIN_ONLY_CONSUMERS"
33 | private const val CompilerFlags = "COMPILER_FLAGS"
34 | private const val R8Rules = "R8_RULES"
35 | private const val ComposeVersion = "COMPOSE_VERSION"
36 | private const val MinApi = "MIN_API"
37 | private const val AutoBuildOnStartup = "AUTO_BUILD_ON_STARTUP"
38 | private const val Presentation = "PRESENTATION"
39 | private const val ShowLineNumbers = "SHOW_LINE_NUMBERS"
40 | private const val ShowByteCode = "SHOW_BYTE_CODE"
41 | private const val ShowDex = "SHOW_DEX"
42 | private const val ShowOat = "SHOW_OAT"
43 | private const val SyncLines = "SYNC_LINES"
44 | private const val Indent = "INDENT"
45 | private const val DecompileHiddenIsa = "DECOMPILE_HIDDEN_ISA"
46 | private const val LineNumberWidth = "LINE_NUMBER_WIDTH"
47 | private const val WindowPosX = "WINDOW_X"
48 | private const val WindowPosY = "WINDOW_Y"
49 | private const val WindowWidth = "WINDOW_WIDTH"
50 | private const val WindowHeight = "WINDOW_HEIGHT"
51 | private const val Placement = "WINDOW_PLACEMENT"
52 |
53 | @Stable
54 | class ExplorerState {
55 | val directory: Path = settingsPath()
56 | private val file: Path = directory.resolve("settings")
57 | private val entries: MutableMap = readSettings(file)
58 |
59 | var androidHome by StringState(AndroidHome, System.getenv("ANDROID_HOME") ?: System.getProperty("user.home"))
60 | var kotlinHome by StringState(KotlinHome, System.getenv("KOTLIN_HOME") ?: System.getProperty("user.home"))
61 | var toolPaths by mutableStateOf(createToolPaths())
62 | var optimize by BooleanState(Optimize, true)
63 | var keepEverything by BooleanState(KeepEverything, true)
64 | var kotlinOnlyConsumers by BooleanState(KotlinOnlyConsumers, true)
65 | var compilerFlags by StringState(CompilerFlags, "")
66 | var r8Rules by StringState(R8Rules, "")
67 | var composeVersion by StringState(ComposeVersion, "1.8.1")
68 | var minApi by IntState(MinApi, 21)
69 | var autoBuildOnStartup by BooleanState(AutoBuildOnStartup, false)
70 | var presentationMode by BooleanState(Presentation, false)
71 | var showLineNumbers by BooleanState(ShowLineNumbers, false)
72 | var showByteCode by BooleanState(ShowByteCode, false)
73 | var showDex by BooleanState(ShowDex, true)
74 | var showOat by BooleanState(ShowOat, true)
75 | var showLogsAndDocumentation by mutableStateOf(false)
76 | var syncLines by BooleanState(SyncLines, true)
77 | var lineNumberWidth by IntState(LineNumberWidth, 4)
78 | var indent by IntState(Indent, 4)
79 | var decompileHiddenIsa by BooleanState(DecompileHiddenIsa, true)
80 | var sourceCode: String = readSourceCode(toolPaths)
81 | var windowWidth by IntState(WindowWidth, 1900)
82 | var windowHeight by IntState(WindowHeight, 1600)
83 | var windowPosX by IntState(WindowPosX, -1)
84 | var windowPosY by IntState(WindowPosY, -1)
85 | var windowPlacement by SettingsState(Placement, Floating) { WindowPlacement.valueOf(this) }
86 |
87 | fun reloadToolPathsFromSettings() {
88 | toolPaths = createToolPaths()
89 | }
90 |
91 | private fun createToolPaths() = ToolPaths(directory, Path.of(androidHome), Path.of(kotlinHome))
92 |
93 | private inner class BooleanState(key: String, initialValue: Boolean) :
94 | SettingsState(key, initialValue, { toBoolean() })
95 |
96 | private inner class IntState(key: String, initialValue: Int) :
97 | SettingsState(key, initialValue, { toInt() })
98 |
99 | private inner class StringState(key: String, initialValue: String) :
100 | SettingsState(key, initialValue, { this })
101 |
102 | private open inner class SettingsState(private val key: String, initialValue: T, parse: String.() -> T) :
103 | MutableState {
104 | private val state = mutableStateOf(entries[key]?.parse() ?: initialValue)
105 | override var value: T
106 | get() = state.value
107 | set(value) {
108 | entries[key] = value.toString()
109 | state.value = value
110 | }
111 |
112 | override fun component1() = state.component1()
113 |
114 | override fun component2() = state.component2()
115 | }
116 |
117 | fun writeSourceCodeState() {
118 | Files.writeString(toolPaths.sourceFile, sourceCode)
119 | }
120 |
121 | fun writeState() {
122 | writeSourceCodeState()
123 | Files.writeString(
124 | file,
125 | entries.map { (key, value) -> "$key=${value.replace("\n", "\\\n")}" }.joinToString("\n")
126 | )
127 | }
128 | }
129 |
130 | private fun settingsPath() = Paths.get(System.getProperty("user.home"), ".kotlin-explorer").apply {
131 | if (!exists()) Files.createDirectory(this)
132 | }
133 |
134 | private fun readSettings(file: Path): MutableMap {
135 | val settings = mutableMapOf()
136 | if (!file.exists()) return settings
137 |
138 | val lines = file.readLines()
139 | var i = 0
140 | while (i < lines.size) {
141 | val line = lines[i]
142 | val index = line.indexOf('=')
143 | if (index != -1) {
144 | var value = line.substring(index + 1)
145 | if (value.endsWith('\\')) {
146 | value = value.dropLast(1) + '\n'
147 | do {
148 | i++
149 | if (i >= lines.size) break
150 | value += lines[i].dropLast(1)
151 | val continuation = lines[i].endsWith('\\')
152 | if (continuation) value += '\n'
153 | } while (continuation)
154 | }
155 | settings[line.substring(0, index)] = value
156 | }
157 | i++
158 | }
159 |
160 | return settings
161 | }
162 |
163 | private fun readSourceCode(toolPaths: ToolPaths) = if (toolPaths.sourceFile.exists()) {
164 | Files.readString(toolPaths.sourceFile)
165 | } else {
166 | """
167 | // NOTE: If Build > Keep Everything is *not* checked, used the @Keep
168 | // annotation to keep the classes/methods/etc. you want to disassemble
169 | fun square(a: Int): Int {
170 | return a * a
171 | }
172 |
173 | """.trimIndent()
174 | }
175 |
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/String.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2024 The Android Open Source Project
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 dev.romainguy.kotlin.explorer
18 |
19 | fun Iterator.consumeUntil(prefix: String): Boolean {
20 | while (hasNext()) {
21 | val line = next()
22 | if (line.trim().startsWith(prefix)) return true
23 | }
24 | return false
25 | }
26 |
27 | fun Iterator.consumeUntil(regex: Regex): MatchResult? {
28 | while (hasNext()) {
29 | val line = next()
30 | val match = regex.matchEntire(line)
31 | if (match != null) {
32 | return match
33 | }
34 | }
35 | return null
36 | }
37 |
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Swing.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Romain Guy
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 | @file:Suppress("FunctionName")
18 |
19 | package dev.romainguy.kotlin.explorer
20 |
21 | import java.awt.Point
22 | import javax.swing.JTextArea
23 | import javax.swing.JViewport
24 |
25 | fun JTextArea.centerCaretInView() {
26 | val viewport = parent as? JViewport ?: return
27 | val linePos = modelToView2D(caretPosition).bounds.centerY.toInt()
28 | viewport.viewPosition = Point(0, maxOf(0, linePos - viewport.height / 2))
29 | }
30 |
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/SyntaxTextArea.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2024 The Android Open Source Project
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 dev.romainguy.kotlin.explorer
18 |
19 | import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea
20 | import javax.swing.JScrollPane
21 | import javax.swing.JViewport
22 |
23 | private const val FontSizeEditingMode = 12.0f
24 | private const val FontSizePresentationMode = 20.0f
25 |
26 | open class SyntaxTextArea : RSyntaxTextArea() {
27 | var presentationMode = false
28 | set(value) {
29 | if (field != value) {
30 | field = value
31 | updateFontSize()
32 | }
33 | }
34 |
35 | private fun updateFontSize() {
36 | val scheme = syntaxScheme
37 |
38 | val increaseRatio = if (presentationMode) {
39 | FontSizePresentationMode / FontSizeEditingMode
40 | } else {
41 | FontSizeEditingMode / FontSizePresentationMode
42 | }
43 |
44 | val count = scheme.styleCount
45 | for (i in 0 until count) {
46 | val ss = scheme.getStyle(i)
47 | if (ss != null) {
48 | val font = ss.font
49 | if (font != null) {
50 | val oldSize: Float = font.size2D
51 | val newSize: Float = oldSize * increaseRatio
52 | ss.font = font.deriveFont(newSize)
53 | }
54 | }
55 | }
56 |
57 | font = font.deriveFont(if (presentationMode) FontSizePresentationMode else FontSizeEditingMode)
58 |
59 | syntaxScheme = scheme
60 | var parent = parent
61 | if (parent is JViewport) {
62 | parent = parent.parent
63 | if (parent is JScrollPane) {
64 | parent.repaint()
65 | }
66 | }
67 | }
68 | }
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Theme.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Romain Guy
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 dev.romainguy.kotlin.explorer
18 |
19 | import androidx.compose.ui.graphics.Color
20 | import org.jetbrains.skiko.SystemTheme
21 |
22 | val IconErrorColor = Color(0xffee4056)
23 | val IconValidColor = Color(0xff3369d6)
24 | val ErrorColor = Color(0xffa04646)
25 | val ProgressColor = Color(0xff3369d6)
26 | val ProgressTrackColor = Color(0xffc4c4c4)
27 |
28 | enum class KotlinExplorerTheme {
29 | Dark, Light, System;
30 |
31 | // TODO: Using currentSystemTheme leads to an UnsatisfiedLinkError with the JetBrains Runtime
32 | fun isDark() = (if (this == System) fromSystemTheme(/* currentSystemTheme */ SystemTheme.LIGHT) else this) == Dark
33 |
34 | companion object {
35 | fun fromSystemTheme(systemTheme: SystemTheme) = if (systemTheme == SystemTheme.LIGHT) Light else Dark
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/build/ByteCodeDecompiler.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2024 Romain Guy
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 dev.romainguy.kotlin.explorer.build
18 |
19 | import dev.romainguy.kotlin.explorer.process
20 | import java.nio.file.Path
21 | import kotlin.io.path.ExperimentalPathApi
22 | import kotlin.io.path.extension
23 | import kotlin.io.path.pathString
24 | import kotlin.io.path.walk
25 |
26 | class ByteCodeDecompiler {
27 | suspend fun decompile(directory: Path) = process(*buildJavapCommand(directory), directory = directory)
28 |
29 | @OptIn(ExperimentalPathApi::class)
30 | private fun buildJavapCommand(directory: Path): Array {
31 | val command = mutableListOf("javap", "-p", "-l", "-c")
32 | val classFiles = directory.walk()
33 | .filter { path -> path.extension == "class" }
34 | .map { path -> directory.relativize(path).pathString }
35 | .sorted()
36 | command.addAll(classFiles)
37 | return command.toTypedArray()
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/build/DexCompiler.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2024 Romain Guy
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 dev.romainguy.kotlin.explorer.build
18 |
19 | import dev.romainguy.kotlin.explorer.ProcessResult
20 | import dev.romainguy.kotlin.explorer.ToolPaths
21 | import dev.romainguy.kotlin.explorer.process
22 | import java.nio.file.Files
23 | import java.nio.file.Path
24 | import kotlin.io.path.*
25 |
26 | class DexCompiler(private val toolPaths: ToolPaths, private val outputDirectory: Path, private val r8rules: String, private val minApi: Int) {
27 | suspend fun buildDex(optimize: Boolean, keepEverything: Boolean): ProcessResult {
28 | return process(*buildDexCommand(optimize, keepEverything), directory = outputDirectory)
29 | }
30 |
31 | suspend fun dumpDex() = process(
32 | toolPaths.dexdump.toString(),
33 | "-d",
34 | "classes.dex",
35 | directory = outputDirectory
36 | )
37 |
38 | @OptIn(ExperimentalPathApi::class)
39 | private fun buildDexCommand(optimize: Boolean, keepEverything: Boolean): Array {
40 | writeR8Rules(keepEverything)
41 |
42 | return buildList {
43 | add("java")
44 | add("-classpath")
45 | add(toolPaths.d8.toString())
46 | add(if (optimize) "com.android.tools.r8.R8" else "com.android.tools.r8.D8")
47 | add("--min-api")
48 | add(minApi.toString())
49 | if (optimize) {
50 | add("--pg-conf")
51 | add("rules.txt")
52 | }
53 | add("--output")
54 | add(".")
55 | add("--lib")
56 | add(toolPaths.platform.toString())
57 | if (!optimize) {
58 | toolPaths.kotlinLibs.forEach { path ->
59 | add("--lib")
60 | add(path.pathString)
61 | }
62 | }
63 | outputDirectory.walk()
64 | .map { path -> outputDirectory.relativize(path) }
65 | .filter { path -> path.extension == "class" && path.first().name != "META-INF" }
66 | .forEach { path -> add(path.pathString) }
67 |
68 | if (optimize) {
69 | addAll(toolPaths.kotlinLibs.map { it.toString() })
70 | }
71 |
72 | }.toTypedArray()
73 | }
74 |
75 | private fun writeR8Rules(keepEverything: Boolean) {
76 | // Match $ANDROID_HOME/tools/proguard/proguard-android-optimize.txt
77 | Files.writeString(
78 | outputDirectory.resolve("rules.txt"),
79 | buildString {
80 | append(
81 | """
82 | -optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
83 | -optimizationpasses 5
84 | -allowaccessmodification
85 | -dontpreverify
86 | -dontobfuscate
87 | """.trimIndent()
88 | )
89 | if (keepEverything) {
90 | append(
91 | """
92 | -keep,allowoptimization class !kotlin.**,!kotlinx.** {
93 | ;
94 | }
95 | """.trimIndent()
96 | )
97 | } else {
98 | append(
99 | """
100 | -keep,allowobfuscation @interface Keep
101 | -keep @Keep class * {*;}
102 | -keepclasseswithmembers class * {
103 | @Keep ;
104 | }
105 | -keepclasseswithmembers class * {
106 | @Keep ;
107 | }
108 | -keepclasseswithmembers class * {
109 | @Keep (...);
110 | }
111 | """.trimIndent()
112 | )
113 | }
114 | append(r8rules)
115 | }
116 | )
117 | }
118 | }
119 |
120 |
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/build/KolinCompiler.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2024 Romain Guy
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 dev.romainguy.kotlin.explorer.build
18 |
19 | import dev.romainguy.kotlin.explorer.DependencyCache
20 | import dev.romainguy.kotlin.explorer.ProcessResult
21 | import dev.romainguy.kotlin.explorer.ToolPaths
22 | import dev.romainguy.kotlin.explorer.process
23 | import java.io.File
24 | import java.nio.file.Path
25 | import kotlin.io.path.useLines
26 |
27 | private val BuiltInFiles = listOf(
28 | "Keep.kt",
29 | "NeverInline.kt"
30 | )
31 |
32 | class KotlinCompiler(
33 | private val toolPaths: ToolPaths,
34 | settingsDirectory: Path,
35 | private val outputDirectory: Path,
36 | ) {
37 | private val dependencyCache = DependencyCache(settingsDirectory.resolve("dependency-cache"))
38 |
39 | suspend fun compile(
40 | kotlinOnlyConsumers: Boolean,
41 | compilerFlags: String,
42 | composeVersion: String,
43 | source: Path
44 | ): ProcessResult {
45 | val sb = StringBuilder()
46 | val result = process(
47 | *buildCompileCommand(
48 | kotlinOnlyConsumers,
49 | compilerFlags,
50 | composeVersion,
51 | source
52 | ) { sb.appendLine(it) }, directory = outputDirectory
53 | )
54 | return ProcessResult(result.exitCode, "$sb\n${result.output}")
55 | }
56 |
57 | private suspend fun buildCompileCommand(
58 | kotlinOnlyConsumers: Boolean,
59 | compilerFlags: String,
60 | composeVersion: String,
61 | file: Path,
62 | onOutput: (String) -> Unit
63 | ): Array {
64 | val isCompose = file.isCompose()
65 | val classpath = buildList {
66 | addAll(toolPaths.kotlinLibs)
67 | add(toolPaths.platform)
68 | if (isCompose) {
69 | add(
70 | dependencyCache.getDependency(
71 | "androidx.compose.runtime",
72 | "runtime-android",
73 | composeVersion,
74 | onOutput,
75 | )
76 | )
77 | }
78 | }.joinToString((File.pathSeparator)) { it.toString() }
79 |
80 | val command = buildList {
81 | add(toolPaths.kotlinc.toString())
82 | add("-Xmulti-platform")
83 | add("-classpath")
84 | add(classpath)
85 | if (kotlinOnlyConsumers) {
86 | this += "-Xno-param-assertions"
87 | this += "-Xno-call-assertions"
88 | this += "-Xno-receiver-assertions"
89 | }
90 | if (compilerFlags.isNotEmpty() || compilerFlags.isNotBlank()) {
91 | // TODO: Do something smarter in case a flag looks like -foo="something with space"
92 | addAll(compilerFlags.split(' '))
93 | }
94 | if (isCompose) {
95 | val composePlugin = dependencyCache.getDependency(
96 | "org.jetbrains.kotlin",
97 | "kotlin-compose-compiler-plugin",
98 | getKotlinVersion(),
99 | onOutput,
100 | )
101 | add("-Xplugin=$composePlugin")
102 | }
103 | // Source code to compile
104 | this += file.toString()
105 | for (fileName in BuiltInFiles) {
106 | this += file.parent.resolve(fileName).toString()
107 | }
108 | }
109 |
110 | return command.toTypedArray()
111 | }
112 |
113 | private suspend fun getKotlinVersion(): String {
114 | val command = mutableListOf(
115 | toolPaths.kotlinc.toString(),
116 | "-version",
117 | ).toTypedArray()
118 | return process(*command).output.substringAfter("kotlinc-jvm ").substringBefore(" ")
119 | }
120 | }
121 |
122 | private fun Path.isCompose(): Boolean {
123 | return useLines { lines ->
124 | lines.filter { it.trim().startsWith("import") }
125 | .any { it.split(" ").last() == "androidx.compose.runtime.Composable" }
126 | }
127 | }
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/bytecode/ByteCodeParser.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2024 Romain Guy
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 dev.romainguy.kotlin.explorer.bytecode
18 |
19 | import androidx.collection.IntIntMap
20 | import androidx.collection.mutableIntIntMapOf
21 | import dev.romainguy.kotlin.explorer.PeekingIterator
22 | import dev.romainguy.kotlin.explorer.code.*
23 | import dev.romainguy.kotlin.explorer.code.CodeContent.Error
24 | import dev.romainguy.kotlin.explorer.code.CodeContent.Success
25 | import dev.romainguy.kotlin.explorer.consumeUntil
26 | import dev.romainguy.kotlin.explorer.getValue
27 | import dev.romainguy.kotlin.explorer.oat.codeToOpAndOperands
28 |
29 | /*
30 | * Example:
31 | * ```
32 | * public final class KotlinExplorerKt {
33 | * ```
34 | */
35 | private val ClassRegex = Regex("^(?.* class [_a-zA-Z][_.$\\w]+).*\\{$")
36 |
37 | /**
38 | * Example:
39 | * ```
40 | * testData.InnerClassKt$main$1();
41 | * ```
42 | */
43 | private val MethodRegex = Regex("^ {2}(?.*\\));$")
44 |
45 | /**
46 | * Example:
47 | *
48 | * ```
49 | * 10: ifne 16
50 | * ```
51 | */
52 |
53 | private val InstructionRegex = Regex("^(?\\d+): +(?.*)$")
54 |
55 | /**
56 | * Examples:
57 | *
58 | * ```
59 | * 4: if_icmpge 31
60 | * 10: ifne 16
61 | * 13: goto 25
62 | * ```
63 | */
64 | private val JumpRegex = Regex("^(goto|if[_a-z]*) +(?\\d+)$")
65 |
66 | private val CommentRegex = Regex("\\s+//")
67 |
68 | class ByteCodeParser {
69 | fun parse(text: String): CodeContent {
70 | return try {
71 | val lines = PeekingIterator(text.lineSequence().iterator())
72 |
73 | val classes = buildList {
74 | while (lines.hasNext()) {
75 | val match = lines.consumeUntil(ClassRegex) ?: break
76 | val clazz = lines.readClass(match.getValue("header"))
77 | add(clazz)
78 | }
79 | }
80 | Success(classes)
81 | } catch (e: Exception) {
82 | Error(e)
83 | }
84 | }
85 | }
86 |
87 | private fun PeekingIterator.readClass(classHeader: String): Class {
88 | val methods = buildList {
89 | while (hasNext()) {
90 | val line = peek()
91 | when {
92 | line == "}" -> break
93 | MethodRegex.matches(line) -> add(readMethod())
94 | else -> next()
95 | }
96 | }
97 | if (next() != "}") {
98 | throw IllegalStateException("Expected '}' but got '${peek()}'")
99 | }
100 | }
101 | return Class(classHeader, methods, false)
102 | }
103 |
104 |
105 | private fun PeekingIterator.readMethod(): Method {
106 | val match = MethodRegex.matchEntire(next())
107 | ?: throw IllegalStateException("Expected method but got '${peek()}'")
108 | val header = match.getValue("header")
109 | // A method can have no code
110 | if (next().trim() != "Code:") return Method(header, InstructionSet(ISA.ByteCode, emptyList()))
111 |
112 | val instructions = readInstructions()
113 | val lineNumbers = readLineNumbers()
114 |
115 | return Method(header, InstructionSet(ISA.ByteCode, instructions.withLineNumbers(lineNumbers)))
116 | }
117 |
118 |
119 | private fun PeekingIterator.readInstructions(): List {
120 | return buildList {
121 | while (hasNext()) {
122 | val line = peek().trim()
123 | val match = InstructionRegex.matchEntire(line) ?: break
124 | next()
125 | val address = match.getValue("address")
126 | val code = match.getValue("code").replace(CommentRegex, " //")
127 | val jumpAddress = JumpRegex.matchEntire(code)?.getValue("address")?.toInt() ?: -1
128 | val (op, operands) = codeToOpAndOperands(code)
129 | add(Instruction(address.toInt(), address, op, operands, jumpAddress))
130 | }
131 | }
132 | }
133 |
134 | private fun PeekingIterator.readLineNumbers(): IntIntMap {
135 | val map = mutableIntIntMapOf()
136 | val found = skipToLineNumberTable()
137 | if (!found) {
138 | return map
139 | }
140 | next()
141 | while (hasNext()) {
142 | val line = peek().trim()
143 | if (!line.startsWith("line")) {
144 | break
145 | }
146 | next()
147 | val (lineNumber, address) = line.substringAfter(' ').split(": ", limit = 2)
148 | map.put(address.toInt(), lineNumber.toInt())
149 | }
150 | return map
151 | }
152 |
153 | private fun PeekingIterator.skipToLineNumberTable(): Boolean {
154 | while (hasNext()) {
155 | val line = peek().trim()
156 | when (line) {
157 | "LineNumberTable:" -> return true
158 | "", "}" -> return false
159 | }
160 | next()
161 | }
162 | return false
163 | }
164 |
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/Code.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2024 Romain Guy
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 dev.romainguy.kotlin.explorer.code
18 |
19 | import androidx.collection.IntIntMap
20 | import androidx.collection.IntObjectMap
21 | import androidx.collection.mutableIntObjectMapOf
22 |
23 | /**
24 | * A data model representing disassembled code
25 | *
26 | * Given a list [Class]'s constructs a mode that provides:
27 | * - Disassembled text with optional line number annotations
28 | * - Jump information for branch instructions
29 | */
30 | class Code(
31 | val isa: ISA,
32 | val text: String,
33 | val instructions: IntObjectMap,
34 | private val jumps: IntIntMap,
35 | private val sourceToCodeLine: IntIntMap,
36 | private val codeToSourceToLine: IntIntMap,
37 | ) {
38 | fun getJumpTargetOfLine(line: Int) = jumps.getOrDefault(line, -1)
39 |
40 | fun getCodeLine(sourceLine: Int) = sourceToCodeLine.getOrDefault(sourceLine, -1)
41 |
42 | fun getSourceLine(codeLine: Int) = codeToSourceToLine.getOrDefault(codeLine, -1)
43 |
44 | companion object {
45 | fun fromClasses(classes: List, codeStyle: CodeStyle = CodeStyle()): Code {
46 | return buildCode(codeStyle) {
47 | val indexedMethods = buildIndexedMethods(classes)
48 | classes.forEachIndexed { classIndex, clazz ->
49 | if (clazz.builtIn) return@forEachIndexed
50 | startClass(clazz)
51 | val notLastClass = classIndex < classes.size - 1
52 | clazz.methods.forEachIndexed { methodIndex, method ->
53 | writeMethod(method, indexedMethods)
54 | if (methodIndex < clazz.methods.size - 1 || notLastClass) writeLine("")
55 | }
56 | }
57 | }.build()
58 | }
59 |
60 | private fun buildIndexedMethods(classes: List): IntObjectMap {
61 | val map = mutableIntObjectMapOf()
62 | classes.forEach { clazz ->
63 | clazz.methods.forEach { method ->
64 | if (method.index != -1) {
65 | map[method.index] = method
66 | }
67 | }
68 | }
69 | return map
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/CodeBuilder.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2024 Romain Guy
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 dev.romainguy.kotlin.explorer.code
18 |
19 | import androidx.collection.IntIntPair
20 | import androidx.collection.IntObjectMap
21 | import androidx.collection.mutableIntIntMapOf
22 | import androidx.collection.mutableIntObjectMapOf
23 | import kotlin.math.max
24 |
25 | fun buildCode(codeStyle: CodeStyle = CodeStyle(), builderAction: CodeBuilder.() -> Unit): CodeBuilder {
26 | return CodeBuilder(codeStyle).apply(builderAction)
27 | }
28 |
29 | /**
30 | * Builds a [Code] model
31 | *
32 | * This class maintains a state allowing it to build the `jump` table `line-number` list
33 | * that will be used by the UI to display jump markers and line number annotations.
34 | */
35 | class CodeBuilder(private val codeStyle: CodeStyle) {
36 | private var line = 0
37 | private val sb = StringBuilder()
38 | private val jumps = mutableIntIntMapOf()
39 | private val sourceToCodeLine = mutableIntIntMapOf()
40 | private val codeToSourceToLine = mutableIntIntMapOf()
41 | private var isa = ISA.Aarch64
42 | private val instructions = mutableIntObjectMapOf()
43 |
44 | // These 3 fields collect method scope data. They are reset when a method is added
45 | private val methodAddresses = mutableIntIntMapOf()
46 | private val methodJumps = mutableListOf()
47 | private var lastMethodLineNumber: Int = -1
48 |
49 | private fun alignOpCodes(isa: ISA) = when (isa) {
50 | ISA.ByteCode -> true
51 | ISA.X86_64 -> true
52 | ISA.Aarch64 -> true
53 | else -> false
54 | }
55 |
56 | fun startClass(clazz: Class) {
57 | writeLine(clazz.header)
58 | }
59 |
60 | fun writeMethod(method: Method, indexedMethods: IntObjectMap) {
61 | startMethod(method)
62 |
63 | val instructionSet = method.instructionSet
64 | // TODO: We should do this only once
65 | isa = instructionSet.isa
66 | val opCodeLength = if (alignOpCodes(instructionSet.isa)) opCodeLength(instructionSet) else -1
67 |
68 | instructionSet.instructions.forEach { instruction ->
69 | writeInstruction(instructionSet, instruction, indexedMethods, opCodeLength)
70 | }
71 |
72 | endMethod()
73 | }
74 |
75 | private fun opCodeLength(instructionSet: InstructionSet): Int {
76 | var maxLength = 0
77 | instructionSet.instructions.forEach { instruction ->
78 | val opCode = instruction.op
79 | maxLength = max(maxLength, opCode.length)
80 | }
81 | return maxLength
82 | }
83 |
84 | private fun startMethod(method: Method) {
85 | sb.append(" ".repeat(codeStyle.indent))
86 | writeLine(method.header)
87 |
88 | val indent = " ".repeat(codeStyle.indent)
89 |
90 | sb.append(indent)
91 | val codeSize = method.codeSize
92 | val instructionCount = method.instructionSet.instructions.size
93 | writeLine("-- $instructionCount instruction${if (instructionCount > 1) "s" else ""}${if (codeSize >= 0) " ($codeSize bytes)" else ""}")
94 |
95 | val (pre, post) = countBranches(method.instructionSet)
96 | val branches = pre + post
97 | if (branches > 0) {
98 | sb.append(indent)
99 | writeLine("-- $branches branch${if (branches > 1) "es" else ""} ($pre + $post)")
100 | }
101 | }
102 |
103 | private fun countBranches(instructionSet: InstructionSet): IntIntPair {
104 | var preReturnCount = 0
105 | var postReturnCount = 0
106 | var returnSeen = false
107 |
108 | val branchInstructions = instructionSet.isa.branchInstructions
109 | val returnInstructions = instructionSet.isa.returnInstructions
110 |
111 | instructionSet.instructions.forEach { instruction ->
112 | val opCode = instruction.op
113 | if (returnInstructions.contains(opCode)) {
114 | returnSeen = true
115 | } else {
116 | if (branchInstructions.contains(opCode)) {
117 | if (returnSeen) {
118 | postReturnCount++
119 | } else {
120 | preReturnCount++
121 | }
122 | }
123 | }
124 | }
125 |
126 | return IntIntPair(preReturnCount, postReturnCount)
127 | }
128 |
129 | private fun endMethod() {
130 | methodJumps.forEach { (line, address) ->
131 | val targetLine = methodAddresses.getOrDefault(address, -1)
132 | if (targetLine == -1) return@forEach
133 | jumps[line] = targetLine
134 | }
135 | methodAddresses.clear()
136 | methodJumps.clear()
137 | lastMethodLineNumber = -1
138 | }
139 |
140 | private fun writeInstruction(
141 | instructionSet: InstructionSet,
142 | instruction: Instruction,
143 | indexedMethods: IntObjectMap,
144 | opCodeLength: Int
145 | ) {
146 | sb.append(" ".repeat(codeStyle.indent))
147 |
148 | methodAddresses[instruction.address] = line
149 | if (instruction.jumpAddress > -1) {
150 | methodJumps.add(IntIntPair(line, instruction.jumpAddress))
151 | }
152 |
153 | val lineNumber = instruction.lineNumber
154 | if (lineNumber > -1) {
155 | sourceToCodeLine[lineNumber] = line
156 | lastMethodLineNumber = lineNumber
157 | }
158 |
159 | codeToSourceToLine[line] = lastMethodLineNumber
160 | if (codeStyle.showLineNumbers) {
161 | val prefix = if (lineNumber > -1) "$lineNumber:" else " "
162 | sb.append(prefix.padEnd(codeStyle.lineNumberWidth + 2))
163 | }
164 |
165 | instructions[line] = instruction
166 |
167 | sb.append(instruction.label)
168 | sb.append(": ")
169 | sb.append(instruction.op)
170 | if (instruction.operands.isNotEmpty()) {
171 | sb.append(if (opCodeLength != -1) " ".repeat(opCodeLength - instruction.op.length + 1) else " ")
172 | sb.append(instruction.operands)
173 | }
174 |
175 | if (instruction.callAddress != -1) {
176 | val set = if (instruction.callAddressMethod == -1) {
177 | instructionSet
178 | } else {
179 | indexedMethods[instruction.callAddressMethod]?.instructionSet
180 | }
181 | val callReference = set?.methodReferences?.get(instruction.callAddress)
182 | if (callReference != null) {
183 | sb.append(" → ").append(callReference.name)
184 | }
185 | }
186 |
187 | sb.append('\n')
188 | line++
189 | }
190 |
191 | fun build(): Code {
192 | return Code(isa, sb.toString(), instructions, jumps, sourceToCodeLine, codeToSourceToLine)
193 | }
194 |
195 | override fun toString() = sb.toString()
196 |
197 | fun writeLine(text: String) {
198 | sb.append(text)
199 | sb.append('\n')
200 | line++
201 | }
202 | }
203 |
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/CodeContent.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2024 Romain Guy
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 dev.romainguy.kotlin.explorer.code
18 |
19 | import java.io.ByteArrayOutputStream
20 | import java.io.PrintStream
21 |
22 | sealed class CodeContent {
23 | data class Success(val classes: List) : CodeContent()
24 | data class Error(val errorText: String) : CodeContent() {
25 | constructor(e: Exception) : this(e.toFullString())
26 | }
27 | data object Empty : CodeContent()
28 | }
29 |
30 | private fun Throwable.toFullString(): String {
31 | return ByteArrayOutputStream().use {
32 | printStackTrace(PrintStream(it))
33 | it.toString()
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/CodeStyle.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2024 Romain Guy
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 dev.romainguy.kotlin.explorer.code
18 |
19 | data class CodeStyle(
20 | val indent: Int = 4,
21 | val showLineNumbers: Boolean = true,
22 | val lineNumberWidth: Int = 4,
23 | )
24 |
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/CodeTextArea.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2024 Romain Guy
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 dev.romainguy.kotlin.explorer.code
18 |
19 | import dev.romainguy.kotlin.explorer.SourceTextArea
20 | import dev.romainguy.kotlin.explorer.SyntaxTextArea
21 | import dev.romainguy.kotlin.explorer.centerCaretInView
22 | import dev.romainguy.kotlin.explorer.code.CodeContent.*
23 | import java.awt.BasicStroke
24 | import java.awt.Graphics
25 | import java.awt.Graphics2D
26 | import java.awt.RenderingHints
27 | import java.awt.event.MouseAdapter
28 | import java.awt.event.MouseEvent
29 | import java.awt.geom.GeneralPath
30 | import javax.swing.event.CaretEvent
31 |
32 | class CodeTextArea(
33 | codeStyle: CodeStyle,
34 | var isSyncLinesEnabled: Boolean,
35 | private val sourceTextArea: SourceTextArea?,
36 | var onLineSelected: ((code: Code, lineNumber: Int, content: String) -> Unit)? = null
37 | ) : SyntaxTextArea() {
38 | private var code: Code? = null
39 | private var jumpOffsets: JumpOffsets? = null
40 | private var content: CodeContent = Empty
41 |
42 | var codeStyle = codeStyle
43 | set(value) {
44 | val changed = value != field
45 | field = value
46 | if (changed) {
47 | updatePreservingCaretLine()
48 | }
49 | }
50 |
51 | init {
52 | addCaretListener(::caretUpdate)
53 | markOccurrences = true
54 | markOccurrencesDelay = 400
55 |
56 | if (sourceTextArea != null) {
57 | addMouseListener(object : MouseAdapter() {
58 | override fun mouseClicked(event: MouseEvent) {
59 | if (isSyncLinesEnabled) {
60 | val codeLine = getLineOfOffset(viewToModel2D(event.point))
61 | val line = code?.getSourceLine(codeLine) ?: return
62 | if (line == -1) return
63 | sourceTextArea.gotoLine(this@CodeTextArea, line - 1)
64 | }
65 | }
66 | })
67 | }
68 | }
69 |
70 |
71 | fun setContent(value: CodeContent) {
72 | content = value
73 | updateContent()
74 | }
75 |
76 | fun gotoSourceLine(sourceLine: Int) {
77 | val line = code?.getCodeLine(sourceLine + 1) ?: return
78 | if (line == -1) return
79 | caretPosition = getLineStartOffset(line.coerceIn(0 until lineCount))
80 | centerCaretInView()
81 | }
82 |
83 | private fun updatePreservingCaretLine() {
84 | val line = getLineOfOffset(caretPosition)
85 | val oldText = text
86 | updateContent()
87 | if (oldText != text) {
88 | caretPosition = getLineStartOffset(line)
89 | caretUpdate(line)
90 | }
91 | }
92 |
93 | private fun updateContent() {
94 | val position = caretPosition
95 | code = null
96 | when (val content = content) {
97 | is Empty -> text = ""
98 | is Error -> text = content.errorText
99 | is Success -> code = Code.fromClasses(content.classes, codeStyle).also {
100 | val text = it.text
101 | if (text != this.text) {
102 | val killEdit = this.text.isEmpty()
103 | replaceRange(text, 0, this.text.length)
104 | if (killEdit) discardAllEdits()
105 | }
106 | }
107 | }
108 | caretPosition = minOf(position, document.length)
109 | }
110 |
111 | override fun paintComponent(g: Graphics?) {
112 | super.paintComponent(g)
113 | jumpOffsets?.let { jump ->
114 | val scale = if (presentationMode) 2 else 1
115 | val padding = 6 * scale
116 | val triangleSize = 8 * scale
117 |
118 | val bounds1 = modelToView2D(jump.src)
119 | val bounds2 = modelToView2D(jump.dst)
120 |
121 | val x1 = bounds1.x.toInt() - padding
122 | val y1 = (bounds1.y + lineHeight / 2).toInt()
123 |
124 | val delta = jump.dst - getLineStartOffset(getLineOfOffset(jump.dst))
125 | val endPadding = if (codeStyle.showLineNumbers && delta < 4) 2 else padding
126 |
127 | val x2 = bounds2.x.toInt() - endPadding
128 | val y2 = (bounds2.y + lineHeight / 2).toInt()
129 |
130 | val x0 = modelToView2D(maxOf(codeStyle.indent * 2 - 4, 1)).x.toInt()
131 |
132 | val g2 = g as Graphics2D
133 | g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
134 | g2.stroke = BasicStroke(scale.toFloat())
135 | g2.drawLine(x1, y1, x0, y1)
136 | g2.drawLine(x0, y1, x0, y2)
137 | g2.drawLine(x0, y2, x2 - triangleSize / 2, y2)
138 |
139 | g2.fill(GeneralPath().apply {
140 | val fx = x2.toFloat()
141 | val fy = y2.toFloat() + 0.5f
142 | val fs = triangleSize.toFloat()
143 | moveTo(fx, fy)
144 | lineTo(fx - fs, fy - fs / 2.0f)
145 | lineTo(fx - fs, fy + fs / 2.0f)
146 | })
147 | }
148 | }
149 |
150 | private fun caretUpdate(event: CaretEvent) {
151 | // We receive two events every time, make sure we react to only one
152 | if (event.javaClass.name != "org.fife.ui.rsyntaxtextarea.RSyntaxTextArea\$RSyntaxTextAreaMutableCaretEvent") {
153 | caretUpdate(getLineOfOffset(minOf(event.dot, document.length)))
154 | }
155 | }
156 |
157 | private fun caretUpdate(line: Int) {
158 | val codeModel = code ?: return
159 | val oldJumpOffsets = jumpOffsets
160 |
161 | val lineContent = getLine(line)
162 | if (onLineSelected != null) {
163 | onLineSelected?.invoke(code!!, line, lineContent)
164 | }
165 |
166 | try {
167 | jumpOffsets = null
168 | val dstLine = codeModel.getJumpTargetOfLine(line)
169 | if (dstLine == -1) return
170 |
171 | val srcOffset = getLineStartOffset(line) + lineContent.countPadding()
172 | val dstOffset = getLineStartOffset(dstLine) + getLine(dstLine).countPadding()
173 | jumpOffsets = JumpOffsets(srcOffset, dstOffset)
174 | } finally {
175 | if (jumpOffsets != oldJumpOffsets) {
176 | repaint()
177 | }
178 | }
179 | }
180 |
181 | private fun getLine(line: Int): String {
182 | val start = getLineStartOffset(line)
183 | val end = getLineEndOffset(line)
184 | return document.getText(start, end - start).trimEnd()
185 | }
186 |
187 | private data class JumpOffsets(val src: Int, val dst: Int)
188 | }
189 |
190 | private fun String.countPadding() = indexOfFirst { it != ' ' }
191 |
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/DataModels.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2024 Romain Guy
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 dev.romainguy.kotlin.explorer.code
18 |
19 | import androidx.collection.*
20 |
21 | enum class ISA(val branchInstructions: ScatterSet, val returnInstructions: ScatterSet) {
22 | ByteCode(scatterSetOf("if"), scatterSetOf("areturn", "ireturn", "lreturn", "dreturn", "freturn", "return")),
23 | Dex(scatterSetOf("if"), scatterSetOf("return")),
24 | Oat(scatterSetOf(), scatterSetOf()),
25 | X86_64(
26 | scatterSetOf(
27 | "je",
28 | "jz",
29 | "jne",
30 | "jnz",
31 | "js",
32 | "jns",
33 | "jg",
34 | "jnle",
35 | "jge",
36 | "jnl",
37 | "jl",
38 | "jnge",
39 | "jle",
40 | "jng",
41 | "ja",
42 | "jnbe",
43 | "jae",
44 | "jnb",
45 | "jb",
46 | "jnae",
47 | "jbe",
48 | "jna"
49 | ),
50 | scatterSetOf("ret")
51 | ),
52 | Aarch64(
53 | scatterSetOf(
54 | "b",
55 | "b.eq",
56 | "b.ne",
57 | "b.cs",
58 | "b.hs",
59 | "b.cc",
60 | "b.lo",
61 | "b.mi",
62 | "b.pl",
63 | "b.vs",
64 | "b.vc",
65 | "b.hi",
66 | "b.ls",
67 | "b.ge",
68 | "b.lt",
69 | "b.gt",
70 | "b.le",
71 | "b.al",
72 | "bl",
73 | "cbz",
74 | "cbnz",
75 | "tbz",
76 | "tbnz"
77 | ),
78 | scatterSetOf("ret")
79 | )
80 | }
81 |
82 | data class Class(val header: String, val methods: List, val builtIn: Boolean)
83 |
84 | const val AccessPublic = 0x00001
85 | const val AccessPrivate = 0x00002
86 | const val AccessStatic = 0x00008
87 | const val AccessFinal = 0x00010
88 | const val AccessSynthetic = 0x01000
89 | const val AccessConstructor = 0x10000
90 |
91 | data class Method(
92 | val header: String,
93 | val instructionSet: InstructionSet,
94 | val index: Int = -1,
95 | val codeSize: Int = -1
96 | )
97 |
98 | data class InstructionSet(
99 | val isa: ISA,
100 | val instructions: List,
101 | val methodReferences: IntObjectMap = emptyIntObjectMap()
102 | )
103 |
104 | data class Instruction(
105 | val address: Int,
106 | val label: String,
107 | val op: String,
108 | val operands: String,
109 | val jumpAddress: Int,
110 | val callAddress: Int = -1,
111 | val callAddressMethod: Int = -1,
112 | val lineNumber: Int = -1
113 | )
114 |
115 | data class MethodReference(val address: Int, val name: String)
116 |
117 | fun List.withLineNumbers(lineNumbers: IntIntMap): List {
118 | return map {
119 | val line = lineNumbers.getOrDefault(it.address, -1)
120 | if (line != -1) it.copy(lineNumber = line) else it
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/Documentation.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Romain Guy
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 | @file:OptIn(ExperimentalJewelApi::class)
18 |
19 | package dev.romainguy.kotlin.explorer.code
20 |
21 | import androidx.collection.mutableScatterMapOf
22 | import kotlinx.coroutines.Dispatchers
23 | import kotlinx.coroutines.withContext
24 | import org.jetbrains.jewel.foundation.ExperimentalJewelApi
25 | import org.jetbrains.jewel.markdown.InlineMarkdown
26 | import org.jetbrains.jewel.markdown.MarkdownBlock
27 | import org.jetbrains.jewel.markdown.processing.MarkdownProcessor
28 |
29 | private val Conditions = mutableScatterMapOf(
30 | "eq" to "equal.",
31 | "ne" to "not equal.",
32 | "cs" to "carry set.",
33 | "cc" to "carry clear.",
34 | "mi" to "negative.",
35 | "pl" to "positive or zero.",
36 | "vs" to "overflow.",
37 | "vc" to "no overflow.",
38 | "hi" to "unsigned higher.",
39 | "ls" to "unsigned lower or same.",
40 | "ge" to "signed greater than or equal.",
41 | "lt" to "signed less than.",
42 | "gt" to "signed greater than.",
43 | "le" to "signed less than or equal."
44 | )
45 |
46 | suspend fun MarkdownProcessor.generateInlineDocumentation(code: Code, line: Int): List {
47 | if (code.isa == ISA.Aarch64) {
48 | val fullOp = code.instructions[line]?.op ?: ""
49 | val op = fullOp.substringBefore('.')
50 | val opDocumentation = Aarch64Docs[op]
51 | if (opDocumentation != null) {
52 | return withContext(Dispatchers.Default) {
53 | val blocks = ArrayList()
54 | blocks += MarkdownBlock.Heading(2, InlineMarkdown.Text(opDocumentation.name))
55 |
56 | val condition = fullOp.substringAfter('.', "")
57 | if (condition.isNotEmpty()) {
58 | val conditionDocumentation = Conditions[condition]
59 | if (conditionDocumentation != null) {
60 | blocks += MarkdownBlock.Paragraph(
61 | InlineMarkdown.StrongEmphasis("**", InlineMarkdown.Text("Condition: ")),
62 | InlineMarkdown.Text(conditionDocumentation)
63 | )
64 | }
65 | }
66 |
67 | blocks += processMarkdownDocument(opDocumentation.documentation)
68 |
69 | blocks += MarkdownBlock.Paragraph(
70 | InlineMarkdown.Link(
71 | opDocumentation.url,
72 | "See full documentation",
73 | InlineMarkdown.Text("See full documentation")
74 | )
75 | )
76 |
77 | blocks
78 | }
79 | }
80 | }
81 | return emptyList()
82 | }
83 |
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/OpCodeDoc.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2024 Romain Guy
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 dev.romainguy.kotlin.explorer.code
18 |
19 | data class OpCodeDoc(val name: String, val documentation: String, val url: String)
20 |
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/SyntaxStyle.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Romain Guy
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 dev.romainguy.kotlin.explorer.code
18 |
19 | import org.fife.ui.rsyntaxtextarea.AbstractTokenMakerFactory
20 | import org.fife.ui.rsyntaxtextarea.TokenMakerFactory
21 |
22 | class SyntaxStyle private constructor() {
23 | companion object {
24 | val Dex: String get() = "text/dex-bytecode"
25 | val ByteCode: String get() = "text/java-bytecode"
26 | val Kotlin: String get() = "text/kotlin"
27 | val Oat: String get() = "text/oat-assembly"
28 |
29 | init {
30 | val factory = TokenMakerFactory.getDefaultInstance() as AbstractTokenMakerFactory
31 | factory.putMapping(ByteCode, DexTokenMaker::class.java.canonicalName)
32 | factory.putMapping(Dex, DexTokenMaker::class.java.canonicalName)
33 | factory.putMapping(Kotlin, KotlinTokenMaker::class.java.canonicalName)
34 | factory.putMapping(Oat, OatTokenMaker::class.java.canonicalName)
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/dex/DexDumpParser.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Romain Guy
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 dev.romainguy.kotlin.explorer.dex
18 |
19 | import androidx.collection.IntIntMap
20 | import androidx.collection.mutableIntIntMapOf
21 | import dev.romainguy.kotlin.explorer.BuiltInKotlinClass
22 | import dev.romainguy.kotlin.explorer.HexDigit
23 | import dev.romainguy.kotlin.explorer.code.*
24 | import dev.romainguy.kotlin.explorer.code.CodeContent.Error
25 | import dev.romainguy.kotlin.explorer.code.CodeContent.Success
26 | import dev.romainguy.kotlin.explorer.consumeUntil
27 | import dev.romainguy.kotlin.explorer.getValue
28 | import dev.romainguy.kotlin.explorer.oat.codeToOpAndOperands
29 |
30 | private val PositionRegex = Regex("^\\s*0x(?[0-9a-f]+) line=(?\\d+)$")
31 |
32 | private val JumpRegex = Regex("^$HexDigit{4}: .* (?$HexDigit{4}) // [+-]$HexDigit{4}$")
33 | private val AccessRegex = Regex("^access\\s+:\\s+0x(?[0-9a-fA-F]+)\\s+\\(.+\\)$")
34 |
35 | private const val ClassStart = "Class #"
36 | private const val ClassEnd = "source_file_idx"
37 | private const val ClassName = "Class descriptor"
38 | private const val Instructions = "insns size"
39 | private const val Positions = "positions"
40 | private const val Access = "access"
41 |
42 | internal class DexDumpParser {
43 | fun parse(text: String): CodeContent {
44 | println(text)
45 | return try {
46 | val lines = text.lineSequence().iterator()
47 | val classes = buildList {
48 | while (lines.consumeUntil(ClassStart)) {
49 | val clazz = lines.readClass()
50 | if (clazz != null && clazz.methods.isNotEmpty()) {
51 | add(clazz)
52 | }
53 | }
54 | }
55 | Success(classes)
56 | } catch (e: Exception) {
57 | Error(e)
58 | }
59 | }
60 |
61 | private fun Iterator.readClass(): Class? {
62 | val className = next().getClassName()
63 | if (className.matches(BuiltInKotlinClass)) {
64 | return null
65 | }
66 | val methods = buildList {
67 | var access = 0
68 | while (hasNext()) {
69 | val line = next().trim()
70 | if (line.startsWith(Access)) access = readAccess(line)
71 | when {
72 | line.startsWith(ClassEnd) -> break
73 | line.startsWith(Instructions) -> add(readMethod(className, access))
74 | }
75 | }
76 | }
77 | return Class("class $className", methods, false)
78 | }
79 |
80 | private fun Iterator.readMethod(className: String, access: Int): Method {
81 | val (name, type) = next().substringAfterLast(".").split(':', limit = 2)
82 | val instructions = readInstructions()
83 |
84 | consumeUntil(Positions)
85 |
86 | val positions = readPositions()
87 | val returnType = returnTypeFromType(type)
88 | val paramTypes = paramTypesFromType(type).joinToString(", ")
89 |
90 | return Method(
91 | "$returnType $className.$name($paramTypes)${accessToString(access)}",
92 | InstructionSet(ISA.Dex, instructions.withLineNumbers(positions))
93 | )
94 | }
95 |
96 | private fun accessToString(access: Int) = if (access and AccessStatic != 0) " // static" else ""
97 |
98 | private fun readAccess(line: String): Int {
99 | val bitField = AccessRegex.matchEntire(line)?.getValue("bitField")
100 | return if (bitField != null) bitField.toInt(16) else 0
101 | }
102 |
103 | private fun Iterator.readInstructions(): List {
104 | return buildList {
105 | while (hasNext()) {
106 | val line = next()
107 | if (line[0] == ' ') {
108 | break
109 | }
110 | val code = line.substringAfter('|')
111 | val address = code.substringBefore(": ")
112 | val jumpAddress = JumpRegex.matchEntire(code)?.getValue("address")
113 | val (op, operands) = codeToOpAndOperands(code.substringAfter(": "))
114 | add(Instruction(address.toInt(16), address, op, operands, jumpAddress?.toInt(16) ?: -1))
115 | }
116 | }
117 | }
118 |
119 | private fun Iterator.readPositions(): IntIntMap {
120 | val map = mutableIntIntMapOf()
121 | while (hasNext()) {
122 | val line = next()
123 | val match = PositionRegex.matchEntire(line) ?: break
124 | map.put(match.getValue("address").toInt(16), match.getValue("line").toInt())
125 | }
126 | return map
127 | }
128 | }
129 |
130 | private fun String.getClassName() =
131 | getValue(ClassName)
132 | .removePrefix("L")
133 | .removeSuffix(";")
134 | .replace('/', '.')
135 |
136 | private fun String.getValue(name: String): String {
137 | if (!trim().startsWith(name)) {
138 | throw IllegalStateException("Expected '$name'")
139 | }
140 | return substringAfter('\'').substringBefore('\'')
141 | }
142 |
143 | private fun paramTypesFromType(type: String): List {
144 | val types = mutableListOf()
145 | val paramTypes = type.substringAfter('(').substringBeforeLast(')')
146 |
147 | var i = 0
148 | while (i < paramTypes.length) {
149 | when (paramTypes[i]) {
150 | '[' -> {
151 | val result = jniTypeToJavaType(paramTypes, i + 1)
152 | types += result.first + "[]"
153 | i = result.second
154 | }
155 | else -> {
156 | val result = jniTypeToJavaType(paramTypes, i)
157 | types += result.first
158 | i = result.second
159 | }
160 | }
161 | }
162 |
163 | return types
164 | }
165 |
166 | private fun jniTypeToJavaType(
167 | type: String,
168 | index: Int
169 | ): Pair {
170 | var endIndex = index
171 | return when (type[index]) {
172 | 'B' -> "byte"
173 | 'C' -> "char"
174 | 'D' -> "double"
175 | 'F' -> "float"
176 | 'I' -> "int"
177 | 'J' -> "long"
178 | 'L' -> {
179 | endIndex = type.indexOf(';', index)
180 | type.substring(index + 1, endIndex)
181 | .replace('/', '.')
182 | .replace("java.lang.", "")
183 | }
184 | 'S' -> "short"
185 | 'V' -> "void"
186 | 'Z' -> "boolean"
187 | else -> ""
188 | } to endIndex + 1
189 | }
190 |
191 | private fun returnTypeFromType(type: String): String {
192 | val index = type.lastIndexOf(')') + 1
193 | return when (type[index]) {
194 | '[' -> {
195 | jniTypeToJavaType(type, index + 1).first + "[]"
196 | }
197 | else -> {
198 | jniTypeToJavaType(type, index).first
199 | }
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/oat/OatDumpParser.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2024 Romain Guy
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 dev.romainguy.kotlin.explorer.oat
18 |
19 | import androidx.collection.IntObjectMap
20 | import androidx.collection.mutableIntObjectMapOf
21 | import dev.romainguy.kotlin.explorer.*
22 | import dev.romainguy.kotlin.explorer.code.*
23 | import dev.romainguy.kotlin.explorer.code.CodeContent.Error
24 | import dev.romainguy.kotlin.explorer.code.CodeContent.Success
25 |
26 | private val CodeStartRegex = Regex("^\\s+CODE: \\(code_offset=0x[a-fA-F0-9]+ size=(?\\d+)\\)([.]{3})?")
27 |
28 | private val ClassNameRegex = Regex("^\\d+: L(?[^;]+); \\(offset=0x$HexDigit+\\) \\(type_idx=\\d+\\).+")
29 | private val MethodRegex = Regex("^\\s+\\d+:\\s+(?.+)\\s+\\(dex_method_idx=(?\\d+)\\)")
30 | private val CodeRegex = Regex("^\\s+0x(?$HexDigit+):\\s+$HexDigit+\\s+(?.+)")
31 |
32 | private val DexCodeRegex = Regex("^\\s+0x(?$HexDigit+):\\s+($HexDigit+\\s+)+\\|\\s+(?.+)")
33 | private val DexMethodInvokeRegex = Regex("^invoke-[^}]+},\\s+\\S+\\s+(?.+)\\s+//.+")
34 |
35 | private val Aarch64JumpRegex = Regex(".+ #[+-]0x$HexDigit+ \\(addr 0x(?$HexDigit+)\\)\$")
36 | private val X86JumpRegex = Regex(".+ [+-]\\d+ \\(0x(?$HexDigit{8})\\)\$")
37 |
38 | private val Aarch64MethodCallRegex = Regex("^blr lr$")
39 | private val X86MethodCallRegex = Regex("^TODO$") // TODO: implement x86
40 |
41 | private val DexMethodReferenceRegex = Regex("^\\s+StackMap.+dex_pc=0x(?$HexDigit+),.+$")
42 | private val DexInlineInfoRegex = Regex("^\\s+InlineInfo.+dex_pc=0x(?$HexDigit+),\\s+method_index=(?$HexDigit+).+$")
43 |
44 | internal class OatDumpParser {
45 | private var isa = ISA.Aarch64
46 |
47 | fun parse(text: String): CodeContent {
48 | return try {
49 | val lines = PeekingIterator(text.lineSequence().iterator())
50 | val isa = when (val set = lines.readInstructionSet()) {
51 | "Arm64" -> ISA.Aarch64
52 | "X86_64" -> ISA.X86_64
53 | else -> throw IllegalStateException("Unknown instruction set: $set")
54 | }
55 | val jumpRegex = when (isa) {
56 | ISA.Aarch64 -> Aarch64JumpRegex
57 | ISA.X86_64 -> X86JumpRegex
58 | else -> throw IllegalStateException("Incompatible ISA: $isa")
59 | }
60 | val methodCallRegex = when (isa) {
61 | ISA.Aarch64 -> Aarch64MethodCallRegex
62 | ISA.X86_64 -> X86MethodCallRegex
63 | else -> throw IllegalStateException("Incompatible ISA: $isa")
64 | }
65 | val classes = buildList {
66 | while (lines.hasNext()) {
67 | val match = lines.consumeUntil(ClassNameRegex) ?: break
68 | val clazz = lines.readClass(
69 | match.getValue("class").replace('/', '.'),
70 | jumpRegex,
71 | methodCallRegex
72 | )
73 | if (clazz != null && clazz.methods.isNotEmpty()) {
74 | add(clazz)
75 | }
76 | }
77 | }
78 | Success(classes)
79 | } catch (e: Exception) {
80 | Error(e)
81 | }
82 | }
83 |
84 | private fun PeekingIterator.readInstructionSet(): String {
85 | consumeUntil("INSTRUCTION SET:")
86 | return next()
87 | }
88 |
89 | private fun PeekingIterator.readClass(
90 | className: String,
91 | jumpRegex: Regex,
92 | methodCallRegex: Regex
93 | ): Class? {
94 | val builtIn = className.matches(BuiltInKotlinClass)
95 | val methods = buildList {
96 | while (hasNext()) {
97 | val line = peek()
98 | when {
99 | ClassNameRegex.matches(line) -> break
100 | else -> {
101 | // Skip to the next line first and then read the method
102 | next()
103 |
104 | val match = MethodRegex.matchEntire(line)
105 | if (match != null) {
106 | add(readMethod(match, jumpRegex, methodCallRegex, builtIn))
107 | }
108 | }
109 | }
110 | }
111 | }
112 | return Class("class $className", methods, builtIn)
113 | }
114 |
115 | private fun PeekingIterator.readMethod(
116 | match: MatchResult,
117 | jumpRegex: Regex,
118 | methodCallRegex: Regex,
119 | builtIn: Boolean = false
120 | ): Method {
121 | consumeUntil("DEX CODE:")
122 | val methodReferences = readMethodReferences()
123 |
124 | val codeStart = consumeUntil(CodeStartRegex)
125 | val codeSize = codeStart?.getValue("codeSize")?.toInt() ?: -1
126 |
127 | // We could also check for the presence of "..." at the end of codeStart
128 | val instructions = if (codeSize == 0) {
129 | // Skip "NO CODE!"
130 | next()
131 | emptyList()
132 | } else {
133 | readNativeInstructions(jumpRegex, methodCallRegex, builtIn)
134 | }
135 |
136 | val method = match.getValue("method")
137 | val index = match.getValue("methodIndex").toInt()
138 |
139 | return Method(method, InstructionSet(isa, instructions, methodReferences), index, codeSize)
140 | }
141 |
142 | private fun PeekingIterator.readMethodReferences(): IntObjectMap {
143 | val map = mutableIntObjectMapOf()
144 | while (hasNext()) {
145 | val match = DexCodeRegex.matchEntire(next())
146 | if (match != null) {
147 | match.toMethodReference()?.apply {
148 | map[address] = this
149 | }
150 | } else {
151 | break
152 | }
153 | }
154 | return map
155 | }
156 |
157 | private fun MatchResult.toMethodReference(): MethodReference? {
158 | val code = getValue("code")
159 | val nameMatch = DexMethodInvokeRegex.matchEntire(code)
160 | if (nameMatch != null) {
161 | val address = getValue("address")
162 | val name = nameMatch.getValue("name")
163 | val pc = address.toInt(16)
164 | return MethodReference(pc, name)
165 | }
166 | return null
167 | }
168 |
169 | private fun PeekingIterator.readNativeInstructions(
170 | jumpRegex: Regex,
171 | methodCallRegex: Regex,
172 | builtIn: Boolean
173 | ): List {
174 | return buildList {
175 | while (hasNext()) {
176 | val line = peek()
177 | when {
178 | line.matches(MethodRegex) -> break
179 | line.matches(ClassNameRegex) -> break
180 | else -> {
181 | val match = CodeRegex.matchEntire(next())
182 | if (match != null) {
183 | if (builtIn) continue
184 | add(
185 | readNativeInstruction(
186 | this@readNativeInstructions,
187 | match,
188 | jumpRegex,
189 | methodCallRegex
190 | )
191 | )
192 | }
193 | }
194 | }
195 | }
196 | }
197 | }
198 |
199 | private fun readNativeInstruction(
200 | iterator: PeekingIterator,
201 | match: MatchResult,
202 | jumpRegex: Regex,
203 | methodCallRegex: Regex
204 | ): Instruction {
205 | val address = match.getValue("address")
206 | val code = match.getValue("code")
207 |
208 | var callAddress = if (methodCallRegex.matches(code)) {
209 | DexMethodReferenceRegex.matchEntire(iterator.peek())?.getValue("callAddress")?.toInt(16) ?: -1
210 | } else {
211 | -1
212 | }
213 |
214 | val callAddressMethod = if (callAddress != -1) {
215 | // Skip the StackMap line
216 | iterator.next()
217 | // Check the InlineInfo if present
218 | var index = -1
219 | do {
220 | val methodIndex = DexInlineInfoRegex.matchEntire(iterator.peek())
221 | if (methodIndex != null) {
222 | callAddress = methodIndex.getValue("callAddress").toInt(16)
223 | index = methodIndex.getValue("methodIndex").toInt()
224 | iterator.next()
225 | }
226 | } while (methodIndex != null)
227 | index
228 | } else {
229 | -1
230 | }
231 |
232 | val jumpAddress = if (callAddress == -1) {
233 | jumpRegex.matchEntire(code)?.getValue("address")?.toInt(16) ?: -1
234 | } else {
235 | -1
236 | }
237 |
238 | val codeAddress = address.toInt(16)
239 | val (op, operands) = codeToOpAndOperands(code)
240 | return Instruction(codeAddress, "0x$address", op, operands, jumpAddress, callAddress, callAddressMethod)
241 | }
242 | }
243 |
244 | internal fun codeToOpAndOperands(code: String): Pair {
245 | val index = code.indexOf(' ')
246 | val opCode = if (index >= 0) code.substring(0, index) else code
247 | val operands = if (index >= 0) code.substring(index + 1).trim() else ""
248 | return Pair(opCode, operands)
249 | }
250 |
--------------------------------------------------------------------------------
/src/jvmMain/resources/icons/done.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/jvmMain/resources/icons/error.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/jvmMain/resources/icons/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romainguy/kotlin-explorer/d783c4acc16ac82655c41760568f3be6181db9c4/src/jvmMain/resources/icons/icon.ico
--------------------------------------------------------------------------------
/src/jvmMain/resources/themes/kotlin_explorer.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/src/jvmMain/resources/themes/kotlin_explorer_disassembly.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/src/jvmTest/kotlin/dev/romainguy/kotlin/explorer/bytecode/ByteCodeParserTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2024 Romain Guy
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 dev.romainguy.kotlin.explorer.bytecode
18 |
19 | import com.google.common.truth.Truth.assertThat
20 | import dev.romainguy.kotlin.explorer.code.Code
21 | import dev.romainguy.kotlin.explorer.testing.Builder
22 | import dev.romainguy.kotlin.explorer.testing.parseSuccess
23 | import org.junit.Rule
24 | import org.junit.Test
25 | import org.junit.rules.TemporaryFolder
26 | import java.nio.file.Path
27 | import kotlin.io.path.readText
28 |
29 | class ByteCodeParserTest {
30 | @get:Rule
31 | val temporaryFolder = TemporaryFolder()
32 |
33 | private val builder by lazy { Builder.getInstance(temporaryFolder.root.toPath()) }
34 | private val byteCodeParser = ByteCodeParser()
35 |
36 | @Test
37 | fun issue_45() {
38 | val content = byteCodeParser.parseSuccess(builder.generateByteCode("Issue_45.kt"))
39 |
40 | val text = Code.fromClasses(content.classes).text
41 |
42 | assertThat(text).isEqualTo(loadTestDataFile("Issue_45-Bytecode.expected"))
43 | }
44 |
45 | @Test
46 | fun tryCatch() {
47 | val content = byteCodeParser.parseSuccess(builder.generateByteCode("TryCatch.kt"))
48 |
49 | val text = Code.fromClasses(content.classes).text
50 |
51 | assertThat(text).isEqualTo(loadTestDataFile("TryCatch-Bytecode.expected"))
52 | }
53 | }
54 |
55 | fun loadTestDataFile(path: String): String {
56 | val cwd = Path.of(System.getProperty("user.dir"))
57 | val testData = cwd.resolve("src/jvmTest/kotlin/testData")
58 | return testData.resolve(path).readText()
59 | }
60 |
--------------------------------------------------------------------------------
/src/jvmTest/kotlin/dev/romainguy/kotlin/explorer/testing/Builder.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2024 Romain Guy
3 | *
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 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
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 dev.romainguy.kotlin.explorer.testing
18 |
19 | import dev.romainguy.kotlin.explorer.ToolPaths
20 | import dev.romainguy.kotlin.explorer.build.ByteCodeDecompiler
21 | import dev.romainguy.kotlin.explorer.build.KotlinCompiler
22 | import dev.romainguy.kotlin.explorer.bytecode.ByteCodeParser
23 | import dev.romainguy.kotlin.explorer.code.CodeContent
24 | import kotlinx.coroutines.runBlocking
25 | import java.nio.file.Path
26 | import java.util.*
27 | import kotlin.io.path.*
28 | import kotlin.test.fail
29 |
30 | interface Builder {
31 | fun generateByteCode(testFile: String): String
32 |
33 | companion object {
34 | fun getInstance(outputDirectory: Path) =
35 | try {
36 | LocalBuilder(outputDirectory)
37 | } catch (_: Throwable) {
38 | System.err.println("Failed to create local builder. Using Github builder")
39 | GithubBuilder()
40 | }
41 | }
42 | }
43 |
44 | class GithubBuilder : Builder {
45 | private val cwd = Path.of(System.getProperty("user.dir"))
46 | private val testData = cwd.resolve("src/jvmTest/kotlin/testData")
47 |
48 | override fun generateByteCode(testFile: String): String {
49 | return testData.resolve(testFile.replace(".kt", ".javap")).readText()
50 | }
51 | }
52 |
53 | class LocalBuilder(private val outputDirectory: Path) : Builder {
54 | private val cwd = Path.of(System.getProperty("user.dir"))
55 | private val testData = cwd.resolve("src/jvmTest/kotlin/testData")
56 | private val toolPaths = createToolsPath()
57 | private val kotlinCompiler = KotlinCompiler(toolPaths, outputDirectory, outputDirectory)
58 | private val byteCodeDecompiler = ByteCodeDecompiler()
59 |
60 | override fun generateByteCode(testFile: String): String {
61 | val path = testData.resolve(testFile)
62 | if (path.notExists()) {
63 | fail("$path does not exists")
64 | }
65 | return runBlocking {
66 | kotlinCompile(path)
67 | val result = byteCodeDecompiler.decompile(outputDirectory)
68 | if (result.exitCode != 0) {
69 | System.err.println(result.output)
70 | fail("javap error")
71 | }
72 |
73 | // Save the fine under so we can examine it when needed
74 | val saveFile = testData.resolve(testFile.replace(".kt", ".javap"))
75 | if (saveFile.parent.notExists()) {
76 | saveFile.parent.createDirectory()
77 | }
78 | saveFile.writeText(result.output)
79 |
80 | result.output
81 | }
82 | }
83 |
84 | private suspend fun kotlinCompile(path: Path) {
85 | val result = kotlinCompiler.compile(true, "", "not-used", path)
86 | if (result.exitCode != 0) {
87 | System.err.println(result.output)
88 | fail("kotlinc error")
89 | }
90 | }
91 |
92 | private fun createToolsPath(): ToolPaths {
93 | val properties = Properties()
94 | properties.load(cwd.resolve("local.properties").reader())
95 |
96 | val kotlinHome = getKotlinHome(properties)
97 | val androidHome = getAndroidHome(properties)
98 | val toolPaths = ToolPaths(Path.of(""), androidHome, kotlinHome)
99 | if (toolPaths.isKotlinHomeValid && toolPaths.isAndroidHomeValid) {
100 | return toolPaths
101 | }
102 | throw IllegalStateException("Invalid ToolsPath: KOTLIN_HOME=${kotlinHome} ANDROID_HOME=$androidHome")
103 | }
104 | }
105 |
106 | private fun getKotlinHome(properties: Properties): Path {
107 | val pathString = System.getenv("KOTLIN_HOME") ?: properties.getProperty("kotlin.home")
108 |
109 | if (pathString == null) {
110 | throw IllegalStateException("Could not find Android SDK")
111 | }
112 | val path = Path.of(pathString)
113 | if (path.notExists()) {
114 | throw IllegalStateException("Could not find Android SDK")
115 | }
116 | return path
117 | }
118 |
119 | private fun getAndroidHome(properties: Properties): Path {
120 | val path =
121 | when (val androidHome: String? = System.getenv("ANDROID_HOME") ?: properties.getProperty("android.home")) {
122 | null -> Path.of(System.getProperty("user.home")).resolve("Android/Sdk")
123 | else -> Path.of(androidHome)
124 | }
125 |
126 | if (path.notExists()) {
127 | throw IllegalStateException("Could not find Android SDK")
128 | }
129 | return path
130 | }
131 |
132 | private fun CodeContent.asSuccess(): CodeContent.Success {
133 | if (this !is CodeContent.Success) {
134 | fail("Expected Success but got: $this")
135 | }
136 | return this
137 | }
138 |
139 | fun ByteCodeParser.parseSuccess(text: String) = parse(text).asSuccess()
--------------------------------------------------------------------------------
/src/jvmTest/kotlin/testData/Issue_45-Bytecode.expected:
--------------------------------------------------------------------------------
1 | final class testData.Issue_45Kt$main$1
2 | testData.Issue_45Kt$main$1()
3 | -- 4 instructions
4 | 0: aload_0
5 | 1: iconst_0
6 | 2: invokespecial #12 // Method kotlin/jvm/internal/Lambda."":(I)V
7 | 5: return
8 |
9 | public final void invoke()
10 | -- 2 instructions
11 | 5: 0: invokestatic #20 // Method testData/Issue_45Kt.f2:()V
12 | 6: 3: return
13 |
14 | public java.lang.Object invoke()
15 | -- 4 instructions
16 | 4: 0: aload_0
17 | 1: invokevirtual #23 // Method invoke:()V
18 | 4: getstatic #29 // Field kotlin/Unit.INSTANCE:Lkotlin/Unit;
19 | 7: areturn
20 |
21 | public final class testData.Issue_45Kt
22 | public static final void main()
23 | -- 4 instructions
24 | 4: 0: getstatic #12 // Field testData/Issue_45Kt$main$1.INSTANCE:LtestData/Issue_45Kt$main$1;
25 | 3: checkcast #14 // class kotlin/jvm/functions/Function0
26 | 6: invokestatic #18 // Method f1:(Lkotlin/jvm/functions/Function0;)V
27 | 7: 9: return
28 |
29 | public static final void f1(kotlin.jvm.functions.Function0)
30 | -- 4 instructions
31 | 9: 0: aload_0
32 | 1: invokeinterface #24, 1 // InterfaceMethod kotlin/jvm/functions/Function0.invoke:()Ljava/lang/Object;
33 | 6: pop
34 | 7: return
35 |
36 | public static final void f2()
37 | -- 5 instructions
38 | 11: 0: ldc #29 // String Hi
39 | 2: getstatic #35 // Field java/lang/System.out:Ljava/io/PrintStream;
40 | 5: swap
41 | 6: invokevirtual #41 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
42 | 12: 9: return
43 |
44 | public static void main(java.lang.String[])
45 | -- 2 instructions
46 | 0: invokestatic #44 // Method main:()V
47 | 3: return
48 |
--------------------------------------------------------------------------------
/src/jvmTest/kotlin/testData/Issue_45.javap:
--------------------------------------------------------------------------------
1 | Compiled from "Issue_45.kt"
2 | final class testData.Issue_45Kt$main$1 extends kotlin.jvm.internal.Lambda implements kotlin.jvm.functions.Function0 {
3 | public static final testData.Issue_45Kt$main$1 INSTANCE;
4 |
5 | testData.Issue_45Kt$main$1();
6 | Code:
7 | 0: aload_0
8 | 1: iconst_0
9 | 2: invokespecial #12 // Method kotlin/jvm/internal/Lambda."":(I)V
10 | 5: return
11 | LocalVariableTable:
12 | Start Length Slot Name Signature
13 | 0 6 0 this LtestData/Issue_45Kt$main$1;
14 |
15 | public final void invoke();
16 | Code:
17 | 0: invokestatic #20 // Method testData/Issue_45Kt.f2:()V
18 | 3: return
19 | LineNumberTable:
20 | line 5: 0
21 | line 6: 3
22 | LocalVariableTable:
23 | Start Length Slot Name Signature
24 | 0 4 0 this LtestData/Issue_45Kt$main$1;
25 |
26 | public java.lang.Object invoke();
27 | Code:
28 | 0: aload_0
29 | 1: invokevirtual #23 // Method invoke:()V
30 | 4: getstatic #29 // Field kotlin/Unit.INSTANCE:Lkotlin/Unit;
31 | 7: areturn
32 | LineNumberTable:
33 | line 4: 0
34 | LocalVariableTable:
35 | Start Length Slot Name Signature
36 | 0 8 0 this LtestData/Issue_45Kt$main$1;
37 |
38 | static {};
39 | Code:
40 | 0: new #2 // class testData/Issue_45Kt$main$1
41 | 3: dup
42 | 4: invokespecial #32 // Method "":()V
43 | 7: putstatic #34 // Field INSTANCE:LtestData/Issue_45Kt$main$1;
44 | 10: return
45 | }
46 | Compiled from "Issue_45.kt"
47 | public final class testData.Issue_45Kt {
48 | public static final void main();
49 | Code:
50 | 0: getstatic #12 // Field testData/Issue_45Kt$main$1.INSTANCE:LtestData/Issue_45Kt$main$1;
51 | 3: checkcast #14 // class kotlin/jvm/functions/Function0
52 | 6: invokestatic #18 // Method f1:(Lkotlin/jvm/functions/Function0;)V
53 | 9: return
54 | LineNumberTable:
55 | line 4: 0
56 | line 7: 9
57 |
58 | public static final void f1(kotlin.jvm.functions.Function0);
59 | Code:
60 | 0: aload_0
61 | 1: invokeinterface #24, 1 // InterfaceMethod kotlin/jvm/functions/Function0.invoke:()Ljava/lang/Object;
62 | 6: pop
63 | 7: return
64 | LineNumberTable:
65 | line 9: 0
66 | LocalVariableTable:
67 | Start Length Slot Name Signature
68 | 0 8 0 f Lkotlin/jvm/functions/Function0;
69 |
70 | public static final void f2();
71 | Code:
72 | 0: ldc #29 // String Hi
73 | 2: getstatic #35 // Field java/lang/System.out:Ljava/io/PrintStream;
74 | 5: swap
75 | 6: invokevirtual #41 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
76 | 9: return
77 | LineNumberTable:
78 | line 11: 0
79 | line 12: 9
80 |
81 | public static void main(java.lang.String[]);
82 | Code:
83 | 0: invokestatic #44 // Method main:()V
84 | 3: return
85 | LocalVariableTable:
86 | Start Length Slot Name Signature
87 | 0 4 0 args [Ljava/lang/String;
88 | }
--------------------------------------------------------------------------------
/src/jvmTest/kotlin/testData/Issue_45.kt:
--------------------------------------------------------------------------------
1 | package testData
2 |
3 | fun main() {
4 | f1() {
5 | f2()
6 | }
7 | }
8 |
9 | fun f1(f: () -> Unit) { f() }
10 | fun f2() {
11 | println("Hi")
12 | }
13 |
--------------------------------------------------------------------------------
/src/jvmTest/kotlin/testData/TryCatch-Bytecode.expected:
--------------------------------------------------------------------------------
1 | public final class testData.TryCatchKt
2 | public static final void main()
3 | -- 9 instructions
4 | 4: 0: aconst_null
5 | 1: astore_0
6 | 5: 2: nop
7 | 6: 3: invokestatic #11 // Method foo:()V
8 | 6: goto 16
9 | 8: 9: astore_1
10 | 9: 10: getstatic #17 // Field java/lang/System.out:Ljava/io/PrintStream;
11 | 13: invokevirtual #22 // Method java/io/PrintStream.println:()V
12 | 11: 16: return
13 |
14 | public static final void foo()
15 | -- 3 instructions
16 | 14: 0: getstatic #17 // Field java/lang/System.out:Ljava/io/PrintStream;
17 | 3: invokevirtual #22 // Method java/io/PrintStream.println:()V
18 | 15: 6: return
19 |
20 | public static void main(java.lang.String[])
21 | -- 2 instructions
22 | 0: invokestatic #29 // Method main:()V
23 | 3: return
24 |
--------------------------------------------------------------------------------
/src/jvmTest/kotlin/testData/TryCatch.javap:
--------------------------------------------------------------------------------
1 | Compiled from "TryCatch.kt"
2 | public final class testData.TryCatchKt {
3 | public static final void main();
4 | Code:
5 | 0: aconst_null
6 | 1: astore_0
7 | 2: nop
8 | 3: invokestatic #11 // Method foo:()V
9 | 6: goto 16
10 | 9: astore_1
11 | 10: getstatic #17 // Field java/lang/System.out:Ljava/io/PrintStream;
12 | 13: invokevirtual #22 // Method java/io/PrintStream.println:()V
13 | 16: return
14 | Exception table:
15 | from to target type
16 | 2 6 9 Class java/lang/Exception
17 | LineNumberTable:
18 | line 4: 0
19 | line 5: 2
20 | line 6: 3
21 | line 8: 9
22 | line 9: 10
23 | line 11: 16
24 | LocalVariableTable:
25 | Start Length Slot Name Signature
26 | 10 6 1 e Ljava/lang/Exception;
27 | 2 15 0 a Ljava/lang/Void;
28 |
29 | public static final void foo();
30 | Code:
31 | 0: getstatic #17 // Field java/lang/System.out:Ljava/io/PrintStream;
32 | 3: invokevirtual #22 // Method java/io/PrintStream.println:()V
33 | 6: return
34 | LineNumberTable:
35 | line 14: 0
36 | line 15: 6
37 |
38 | public static void main(java.lang.String[]);
39 | Code:
40 | 0: invokestatic #29 // Method main:()V
41 | 3: return
42 | LocalVariableTable:
43 | Start Length Slot Name Signature
44 | 0 4 0 args [Ljava/lang/String;
45 | }
--------------------------------------------------------------------------------
/src/jvmTest/kotlin/testData/TryCatch.kt:
--------------------------------------------------------------------------------
1 | package testData
2 |
3 | fun main() {
4 | val a = null
5 | try {
6 | foo()
7 | }
8 | catch (e: Exception) {
9 | println()
10 | }
11 | }
12 |
13 | fun foo() {
14 | println()
15 | }
16 |
--------------------------------------------------------------------------------
/token-makers/build.gradle.kts:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage")
2 |
3 | plugins {
4 | id("java")
5 | }
6 |
7 | group = "dev.romainguy.kotlin.explorer.code"
8 | version = "1.0"
9 |
10 | repositories {
11 | mavenCentral()
12 | }
13 |
14 | dependencies {
15 | implementation(libs.rsyntaxtextarea)
16 | implementation(libs.rstaui)
17 | }
18 |
19 | java {
20 | toolchain {
21 | vendor = JvmVendorSpec.JETBRAINS
22 | languageVersion = JavaLanguageVersion.of(17)
23 | }
24 | }
--------------------------------------------------------------------------------
/token-makers/src/main/java/dev/romainguy/kotlin/explorer/code/DexTokenMaker.flex:
--------------------------------------------------------------------------------
1 | package dev.romainguy.kotlin.explorer.code;
2 |
3 | import java.io.*;
4 | import javax.swing.text.Segment;
5 |
6 | import org.fife.ui.rsyntaxtextarea.*;
7 | %%
8 |
9 | %public
10 | %class DexTokenMaker
11 | %extends AbstractJFlexTokenMaker
12 | %unicode
13 | %ignorecase
14 | %type org.fife.ui.rsyntaxtextarea.Token
15 |
16 | %{
17 | public DexTokenMaker() {
18 | super();
19 | }
20 |
21 | private void addToken(int tokenType) {
22 | addToken(zzStartRead, zzMarkedPos-1, tokenType);
23 | }
24 |
25 | private void addToken(int start, int end, int tokenType) {
26 | int so = start + offsetShift;
27 | addToken(zzBuffer, start,end, tokenType, so);
28 | }
29 |
30 | @Override
31 | public void addToken(char[] array, int start, int end, int tokenType, int startOffset) {
32 | super.addToken(array, start,end, tokenType, startOffset);
33 | zzStartRead = zzMarkedPos;
34 | }
35 |
36 | @Override
37 | public String[] getLineCommentStartAndEnd(int languageIndex) {
38 | return new String[] { ";", null };
39 | }
40 |
41 | @Override
42 | public boolean getMarkOccurrencesOfTokenType(int type) {
43 | return type == Token.RESERVED_WORD || type == Token.FUNCTION || type == Token.VARIABLE;
44 | }
45 |
46 | public Token getTokenList(Segment text, int initialTokenType, int startOffset) {
47 |
48 | resetTokenList();
49 | this.offsetShift = -text.offset + startOffset;
50 |
51 | // Start off in the proper state.
52 | int state = Token.NULL;
53 |
54 | s = text;
55 | try {
56 | yyreset(zzReader);
57 | yybegin(state);
58 | return yylex();
59 | } catch (IOException ioe) {
60 | ioe.printStackTrace();
61 | return new TokenImpl();
62 | }
63 |
64 | }
65 |
66 | private boolean zzRefill() {
67 | return zzCurrentPos>=s.offset+s.count;
68 | }
69 |
70 | public final void yyreset(Reader reader) {
71 | // 's' has been updated.
72 | zzBuffer = s.array;
73 | /*
74 | * We replaced the line below with the two below it because zzRefill
75 | * no longer "refills" the buffer (since the way we do it, it's always
76 | * "full" the first time through, since it points to the segment's
77 | * array). So, we assign zzEndRead here.
78 | */
79 | //zzStartRead = zzEndRead = s.offset;
80 | zzStartRead = s.offset;
81 | zzEndRead = zzStartRead + s.count - 1;
82 | zzCurrentPos = zzMarkedPos = zzPushbackPos = s.offset;
83 | zzLexicalState = YYINITIAL;
84 | zzReader = reader;
85 | zzAtBOL = true;
86 | zzAtEOF = false;
87 | }
88 | %}
89 |
90 | Letter = ([A-Za-z_])
91 | LowerCaseLetter = ([a-z])
92 | Digit = ([0-9])
93 | Number = (({Digit}|{LowerCaseLetter})+)
94 |
95 | Identifier = (({Letter}|{Digit})[^ \t\f\n\,\.\+\-\*\/\%\[\]]+)
96 |
97 | OpCode = ({LowerCaseLetter}({LowerCaseLetter}|{Digit})*[^ \t\f\n\,\.\+\*\%\[\]]+)
98 |
99 | UnclosedStringLiteral = ([\"][^\"]*)
100 | StringLiteral = ({UnclosedStringLiteral}[\"])
101 | UnclosedCharLiteral = ([\'][^\']*)
102 | CharLiteral = ({UnclosedCharLiteral}[\'])
103 |
104 | CommentBegin = ("//")
105 | MetadataBegin = ("--")
106 |
107 | LineTerminator = (\n)
108 | WhiteSpace = ([ \t\f])
109 |
110 | Label = ({Digit}({Letter}|{Digit})*[\:])
111 |
112 | %state CODE
113 | %state CLASS
114 | %state FUNCTION_SIGNATURE
115 |
116 | %%
117 |
118 | {
119 | "class" { addToken(Token.RESERVED_WORD_2); yybegin(CLASS); }
120 |
121 | {LineTerminator} { addNullToken(); return firstToken; }
122 |
123 | {WhiteSpace}+ { addToken(Token.WHITESPACE); }
124 |
125 | {Label} { addToken(Token.PREPROCESSOR); yybegin(CODE); }
126 |
127 | ^{WhiteSpace}+{Letter}({Letter}|{Digit}|[.])* {
128 | addToken(Token.DATA_TYPE);
129 | yybegin(FUNCTION_SIGNATURE);
130 | }
131 |
132 | {MetadataBegin}.* { addToken(Token.MARKUP_CDATA); addNullToken(); return firstToken; }
133 | {CommentBegin}.* { addToken(Token.COMMENT_EOL); addNullToken(); return firstToken; }
134 |
135 | <> { addNullToken(); return firstToken; }
136 |
137 | {Identifier} { addToken(Token.IDENTIFIER); }
138 | . { addToken(Token.IDENTIFIER); }
139 | }
140 |
141 | {
142 | {LineTerminator} { addNullToken(); return firstToken; }
143 |
144 | {WhiteSpace}+ { addToken(Token.WHITESPACE); }
145 |
146 | {CommentBegin}.* { addToken(Token.COMMENT_EOL); addNullToken(); return firstToken; }
147 |
148 | <> { addNullToken(); return firstToken; }
149 |
150 | {Identifier} { addToken(Token.FUNCTION); }
151 | . { addToken(Token.IDENTIFIER); }
152 | }
153 |
154 | {
155 | {LineTerminator} { addNullToken(); return firstToken; }
156 |
157 | {WhiteSpace}+ { addToken(Token.WHITESPACE); }
158 |
159 | {CommentBegin}.* { addToken(Token.COMMENT_EOL); addNullToken(); return firstToken; }
160 |
161 | {Letter}({Letter}|{Digit}|[$.\<\>])+ {
162 | addToken(Token.FUNCTION);
163 | }
164 |
165 | (-({Letter}|{Digit}|[$.])+) {
166 | addToken(Token.COMMENT_MULTILINE);
167 | }
168 |
169 | ([\(].+[\)]) { addToken(Token.IDENTIFIER); }
170 |
171 | <> { addNullToken(); return firstToken; }
172 |
173 | {Identifier} { addToken(Token.IDENTIFIER); }
174 | . { addToken(Token.IDENTIFIER); }
175 | }
176 |
177 | {
178 | "#int" |
179 | "#long" |
180 | "#char" |
181 | "#short" |
182 | "#double" |
183 | "#float" { addToken(Token.DATA_TYPE); }
184 |
185 | /* Registers */
186 | "v0" |
187 | "v1" |
188 | "v2" |
189 | "v3" |
190 | "v4" |
191 | "v5" |
192 | "v6" |
193 | "v7" |
194 | "v8" |
195 | "v9" |
196 | "v10" |
197 | "v11" |
198 | "v12" |
199 | "v13" |
200 | "v14" |
201 | "v15" |
202 | "v16" { addToken(Token.VARIABLE); }
203 | }
204 |
205 | {
206 | {CharLiteral} { addToken(Token.LITERAL_CHAR); }
207 | {UnclosedCharLiteral} { addToken(Token.ERROR_CHAR); }
208 | {StringLiteral} { addToken(Token.LITERAL_STRING_DOUBLE_QUOTE); }
209 | {UnclosedStringLiteral} { addToken(Token.ERROR_STRING_DOUBLE); addNullToken(); return firstToken; }
210 |
211 | {CommentBegin}.* { addToken(Token.COMMENT_EOL); addNullToken(); return firstToken; }
212 |
213 | {Label} { addToken(Token.PREPROCESSOR); }
214 |
215 | ([{].+[}]) { addToken(Token.VARIABLE); }
216 |
217 | {OpCode} { addToken(Token.RESERVED_WORD); }
218 |
219 | (L[^ \t\f]+) { addToken(Token.IDENTIFIER); }
220 |
221 | {Number} { addToken(Token.LITERAL_NUMBER_DECIMAL_INT); }
222 |
223 | <> { addNullToken(); return firstToken; }
224 |
225 | {Identifier} { addToken(Token.IDENTIFIER); }
226 | . { addToken(Token.IDENTIFIER); }
227 | }
--------------------------------------------------------------------------------
/token-makers/src/main/java/dev/romainguy/kotlin/explorer/code/OatTokenMaker.flex:
--------------------------------------------------------------------------------
1 | package dev.romainguy.kotlin.explorer.code;
2 |
3 | import java.io.*;
4 | import javax.swing.text.Segment;
5 |
6 | import org.fife.ui.rsyntaxtextarea.*;
7 | %%
8 |
9 | %public
10 | %class OatTokenMaker
11 | %extends AbstractJFlexTokenMaker
12 | %unicode
13 | %ignorecase
14 | %type org.fife.ui.rsyntaxtextarea.Token
15 |
16 | %{
17 | public OatTokenMaker() {
18 | super();
19 | }
20 |
21 | private void addToken(int tokenType) {
22 | addToken(zzStartRead, zzMarkedPos-1, tokenType);
23 | }
24 |
25 | private void addToken(int start, int end, int tokenType) {
26 | int so = start + offsetShift;
27 | addToken(zzBuffer, start,end, tokenType, so);
28 | }
29 |
30 | @Override
31 | public void addToken(char[] array, int start, int end, int tokenType, int startOffset) {
32 | super.addToken(array, start,end, tokenType, startOffset);
33 | zzStartRead = zzMarkedPos;
34 | }
35 |
36 | @Override
37 | public String[] getLineCommentStartAndEnd(int languageIndex) {
38 | return new String[] { ";", "→", null };
39 | }
40 |
41 | @Override
42 | public boolean getMarkOccurrencesOfTokenType(int type) {
43 | return type == Token.RESERVED_WORD || type == Token.FUNCTION || type == Token.VARIABLE;
44 | }
45 |
46 | public Token getTokenList(Segment text, int initialTokenType, int startOffset) {
47 |
48 | resetTokenList();
49 | this.offsetShift = -text.offset + startOffset;
50 |
51 | // Start off in the proper state.
52 | int state = Token.NULL;
53 |
54 | s = text;
55 | try {
56 | yyreset(zzReader);
57 | yybegin(state);
58 | return yylex();
59 | } catch (IOException ioe) {
60 | ioe.printStackTrace();
61 | return new TokenImpl();
62 | }
63 |
64 | }
65 |
66 | private boolean zzRefill() {
67 | return zzCurrentPos>=s.offset+s.count;
68 | }
69 |
70 | public final void yyreset(Reader reader) {
71 | // 's' has been updated.
72 | zzBuffer = s.array;
73 | /*
74 | * We replaced the line below with the two below it because zzRefill
75 | * no longer "refills" the buffer (since the way we do it, it's always
76 | * "full" the first time through, since it points to the segment's
77 | * array). So, we assign zzEndRead here.
78 | */
79 | //zzStartRead = zzEndRead = s.offset;
80 | zzStartRead = s.offset;
81 | zzEndRead = zzStartRead + s.count - 1;
82 | zzCurrentPos = zzMarkedPos = zzPushbackPos = s.offset;
83 | zzLexicalState = YYINITIAL;
84 | zzReader = reader;
85 | zzAtBOL = true;
86 | zzAtEOF = false;
87 | }
88 | %}
89 |
90 | Letter = ([A-Za-z_])
91 | HexLetter = ([A-Fa-f])
92 | LowerCaseLetter = ([a-z])
93 | Digit = ([0-9])
94 | Number = ({Digit}+)
95 | HexNumber = (0x({Digit}|{HexLetter})+)
96 |
97 | Operator = ([ \t\f\n\#\,\.\+\-\*\/\%\[\]\(\)])
98 |
99 | Identifier = (({Letter}|{Digit})[^ \t\f\n\,\.\+\-\*\/\%\[\]]+)
100 |
101 | OpCode = ({LowerCaseLetter}+)
102 |
103 | UnclosedStringLiteral = ([\"][^\"]*)
104 | StringLiteral = ({UnclosedStringLiteral}[\"])
105 | UnclosedCharLiteral = ([\'][^\']*)
106 | CharLiteral = ({UnclosedCharLiteral}[\'])
107 |
108 | CommentBegin = ((";")|("//")|("→"))
109 | MetadataBegin = ("--")
110 |
111 | LineTerminator = (\n)
112 | WhiteSpace = ([ \t\f])
113 |
114 | Label = (0x({Digit}|{HexLetter})+[\:])
115 |
116 | %state CODE
117 | %state CLASS
118 | %state FUNCTION_SIGNATURE
119 |
120 | %%
121 |
122 | {
123 | "class" { addToken(Token.RESERVED_WORD_2); yybegin(CLASS); }
124 |
125 | {LineTerminator} { addNullToken(); return firstToken; }
126 |
127 | {WhiteSpace}+ { addToken(Token.WHITESPACE); }
128 |
129 | {Label} { addToken(Token.PREPROCESSOR); yybegin(CODE); }
130 |
131 | ^{WhiteSpace}+{Letter}({Letter}|{Digit}|[.])* {
132 | addToken(Token.DATA_TYPE);
133 | yybegin(FUNCTION_SIGNATURE);
134 | }
135 |
136 | {MetadataBegin}.* { addToken(Token.MARKUP_CDATA); addNullToken(); return firstToken; }
137 | {CommentBegin}.* { addToken(Token.COMMENT_EOL); addNullToken(); return firstToken; }
138 |
139 | <> { addNullToken(); return firstToken; }
140 |
141 | {Identifier} { addToken(Token.IDENTIFIER); }
142 | . { addToken(Token.IDENTIFIER); }
143 | }
144 |
145 | {
146 | {LineTerminator} { addNullToken(); return firstToken; }
147 |
148 | {WhiteSpace}+ { addToken(Token.WHITESPACE); }
149 |
150 | {CommentBegin}.* { addToken(Token.COMMENT_EOL); addNullToken(); return firstToken; }
151 |
152 | <> { addNullToken(); return firstToken; }
153 |
154 | {Identifier} { addToken(Token.FUNCTION); }
155 | . { addToken(Token.IDENTIFIER); }
156 | }
157 |
158 | {
159 | {LineTerminator} { addNullToken(); return firstToken; }
160 |
161 | {WhiteSpace}+ { addToken(Token.WHITESPACE); }
162 |
163 | {CommentBegin}.* { addToken(Token.COMMENT_EOL); addNullToken(); return firstToken; }
164 |
165 | {Letter}({Letter}|{Digit}|[$.\<\>])+ {
166 | addToken(Token.FUNCTION);
167 | }
168 |
169 | (-({Letter}|{Digit}|[$.])+) {
170 | addToken(Token.COMMENT_MULTILINE);
171 | }
172 |
173 | ([\(].+[\)]) { addToken(Token.IDENTIFIER); }
174 |
175 | <> { addNullToken(); return firstToken; }
176 |
177 | {Identifier} { addToken(Token.IDENTIFIER); }
178 | . { addToken(Token.IDENTIFIER); }
179 | }
180 |
181 | {
182 | "addr" { addToken(Token.DATA_TYPE); }
183 |
184 | "sp" |
185 | "pc" |
186 | "zr" |
187 | "xzr" |
188 | "wzr" |
189 | "lr" |
190 | "nzcv" |
191 | "fpcr" |
192 | "fpsr" |
193 | "daif" |
194 | "w0" |
195 | "w1" |
196 | "w2" |
197 | "w3" |
198 | "w4" |
199 | "w5" |
200 | "w6" |
201 | "w7" |
202 | "w8" |
203 | "w9" |
204 | "w10" |
205 | "w11" |
206 | "w12" |
207 | "w13" |
208 | "w14" |
209 | "w15" |
210 | "w16" |
211 | "w17" |
212 | "w18" |
213 | "w19" |
214 | "w20" |
215 | "w21" |
216 | "w22" |
217 | "w23" |
218 | "w24" |
219 | "w25" |
220 | "w26" |
221 | "w27" |
222 | "w28" |
223 | "w29" |
224 | "w30" |
225 | "w31" |
226 | "r0" |
227 | "r1" |
228 | "r2" |
229 | "r3" |
230 | "r4" |
231 | "r5" |
232 | "r6" |
233 | "r7" |
234 | "r8" |
235 | "r9" |
236 | "r10" |
237 | "r11" |
238 | "r12" |
239 | "r13" |
240 | "r14" |
241 | "r15" |
242 | "r16" |
243 | "r17" |
244 | "r18" |
245 | "r19" |
246 | "r20" |
247 | "r21" |
248 | "r22" |
249 | "r23" |
250 | "r24" |
251 | "r25" |
252 | "r26" |
253 | "r27" |
254 | "r28" |
255 | "r29" |
256 | "r30" |
257 | "r31" |
258 | "s0" |
259 | "s1" |
260 | "s2" |
261 | "s3" |
262 | "s4" |
263 | "s5" |
264 | "s6" |
265 | "s7" |
266 | "s8" |
267 | "s9" |
268 | "s10" |
269 | "s11" |
270 | "s12" |
271 | "s13" |
272 | "s14" |
273 | "s15" |
274 | "s16" |
275 | "s17" |
276 | "s18" |
277 | "s19" |
278 | "s20" |
279 | "s21" |
280 | "s22" |
281 | "s23" |
282 | "s24" |
283 | "s25" |
284 | "s26" |
285 | "s27" |
286 | "s28" |
287 | "s29" |
288 | "s30" |
289 | "s31" |
290 | "d0" |
291 | "d1" |
292 | "d2" |
293 | "d3" |
294 | "d4" |
295 | "d5" |
296 | "d6" |
297 | "d7" |
298 | "d8" |
299 | "d9" |
300 | "d10" |
301 | "d11" |
302 | "d12" |
303 | "d13" |
304 | "d14" |
305 | "d15" |
306 | "d16" |
307 | "d17" |
308 | "d18" |
309 | "d19" |
310 | "d20" |
311 | "d21" |
312 | "d22" |
313 | "d23" |
314 | "d24" |
315 | "d25" |
316 | "d26" |
317 | "d27" |
318 | "d28" |
319 | "d29" |
320 | "d30" |
321 | "d31" |
322 | "b0" |
323 | "b1" |
324 | "b2" |
325 | "b3" |
326 | "b4" |
327 | "b5" |
328 | "b6" |
329 | "b7" |
330 | "b8" |
331 | "b9" |
332 | "b10" |
333 | "b11" |
334 | "b12" |
335 | "b13" |
336 | "b14" |
337 | "b15" |
338 | "b16" |
339 | "b17" |
340 | "b18" |
341 | "b19" |
342 | "b20" |
343 | "b21" |
344 | "b22" |
345 | "b23" |
346 | "b24" |
347 | "b25" |
348 | "b26" |
349 | "b27" |
350 | "b28" |
351 | "b29" |
352 | "b30" |
353 | "b31" |
354 | "v0" |
355 | "v1" |
356 | "v2" |
357 | "v3" |
358 | "v4" |
359 | "v5" |
360 | "v6" |
361 | "v7" |
362 | "v8" |
363 | "v9" |
364 | "v10" |
365 | "v11" |
366 | "v12" |
367 | "v13" |
368 | "v14" |
369 | "v15" |
370 | "v16" |
371 | "v17" |
372 | "v18" |
373 | "v19" |
374 | "v20" |
375 | "v21" |
376 | "v22" |
377 | "v23" |
378 | "v24" |
379 | "v25" |
380 | "v26" |
381 | "v27" |
382 | "v28" |
383 | "v29" |
384 | "v30" |
385 | "v31" |
386 | "x0" |
387 | "x1" |
388 | "x2" |
389 | "x3" |
390 | "x4" |
391 | "x5" |
392 | "x6" |
393 | "x7" |
394 | "x8" |
395 | "x9" |
396 | "x10" |
397 | "x11" |
398 | "x12" |
399 | "x13" |
400 | "x14" |
401 | "x15" |
402 | "x16" |
403 | "x17" |
404 | "x18" |
405 | "x19" |
406 | "x20" |
407 | "x21" |
408 | "x22" |
409 | "x23" |
410 | "x24" |
411 | "x25" |
412 | "x26" |
413 | "x27" |
414 | "x28" |
415 | "x29" |
416 | "x30" |
417 | "x31" { addToken(Token.VARIABLE); }
418 |
419 | "oshld" |
420 | "oshst" |
421 | "nshld" |
422 | "nshst" |
423 | "ishld" |
424 | "ishst" |
425 | "ld" |
426 | "st" |
427 | "sy" |
428 | "eq" |
429 | "ne" |
430 | "cs" |
431 | "hs" |
432 | "cc" |
433 | "lo" |
434 | "mi" |
435 | "pl" |
436 | "vs" |
437 | "vc" |
438 | "hi" |
439 | "ls" |
440 | "ge" |
441 | "lt" |
442 | "gt" |
443 | "le" |
444 | "al" { addToken(Token.RESERVED_WORD_2); }
445 | }
446 |
447 | {
448 | {CharLiteral} { addToken(Token.LITERAL_CHAR); }
449 | {UnclosedCharLiteral} { addToken(Token.ERROR_CHAR); }
450 | {StringLiteral} { addToken(Token.LITERAL_STRING_DOUBLE_QUOTE); }
451 | {UnclosedStringLiteral} { addToken(Token.ERROR_STRING_DOUBLE); addNullToken(); return firstToken; }
452 |
453 | {CommentBegin}.* { addToken(Token.COMMENT_EOL); addNullToken(); return firstToken; }
454 |
455 | {OpCode} { addToken(Token.RESERVED_WORD); }
456 |
457 | {HexNumber} { addToken(Token.LITERAL_NUMBER_HEXADECIMAL); }
458 | {Number} { addToken(Token.LITERAL_NUMBER_DECIMAL_INT); }
459 |
460 | <> { addNullToken(); return firstToken; }
461 |
462 | {Operator} { addToken(Token.OPERATOR); }
463 | [^] { addToken(Token.IDENTIFIER); }
464 | }
--------------------------------------------------------------------------------