Auto-generate the AST.

This commit is contained in:
2023-09-04 21:50:27 -07:00
parent f06ea93dc4
commit 174d51ca1c
58 changed files with 741 additions and 390 deletions

37
buildext/build.gradle.kts Normal file
View File

@ -0,0 +1,37 @@
plugins {
`kotlin-dsl`
}
repositories {
mavenCentral()
gradlePluginPortal()
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10")
implementation("org.jetbrains.kotlin:kotlin-serialization:1.9.10")
implementation("com.fasterxml.jackson.core:jackson-databind:2.15.2")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.2")
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.15.2")
}
gradlePlugin {
plugins {
create("pork_ast") {
id = "gay.pizza.pork.ast"
implementationClass = "gay.pizza.pork.buildext.PorkAstPlugin"
displayName = "Pork AST"
description = "AST generation code for pork"
}
create("pork_module") {
id = "gay.pizza.pork.module"
implementationClass = "gay.pizza.pork.buildext.PorkModulePlugin"
displayName = "Pork Module"
description = "Module convention for pork"
}
}
}

View File

@ -0,0 +1,29 @@
package gay.pizza.pork.buildext
import gay.pizza.pork.buildext.ast.AstCodegen
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.OutputDirectory
import java.io.File
open class GenerateAstCode : DefaultTask() {
init {
outputs.upToDateWhen { false }
}
@get:InputFile
var astDescriptionFile: File = project.file("src/main/ast/pork.yml")
@get:Input
var codePackage: String = "gay.pizza.pork.ast"
@get:OutputDirectory
var outputDirectory: File = project.file("src/main/kotlin/gay/pizza/pork/ast")
@TaskAction
fun generate() {
AstCodegen.run(codePackage, astDescriptionFile.toPath(), outputDirectory.toPath())
}
}

View File

@ -0,0 +1,10 @@
package gay.pizza.pork.buildext
import org.gradle.api.Plugin
import org.gradle.api.Project
class PorkAstPlugin : Plugin<Project> {
override fun apply(target: Project) {
target.tasks.create("generateAstCode", GenerateAstCode::class.java)
}
}

View File

@ -0,0 +1,35 @@
package gay.pizza.pork.buildext
import org.gradle.api.JavaVersion
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
open class PorkModulePlugin : Plugin<Project> {
override fun apply(target: Project) {
target.apply(plugin = "org.jetbrains.kotlin.jvm")
target.apply(plugin = "org.jetbrains.kotlin.plugin.serialization")
target.repositories.mavenCentral()
target.extensions.getByType<JavaPluginExtension>().apply {
val javaVersion = JavaVersion.toVersion(17)
sourceCompatibility = javaVersion
targetCompatibility = javaVersion
}
target.tasks.withType<KotlinCompile> {
kotlinOptions.jvmTarget = "17"
}
target.dependencies {
add("implementation", "org.jetbrains.kotlin:kotlin-bom")
add("implementation", "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
}
}
}

View File

@ -0,0 +1,370 @@
package gay.pizza.pork.buildext.ast
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import com.fasterxml.jackson.module.kotlin.KotlinModule
import gay.pizza.pork.buildext.codegen.*
import java.nio.charset.StandardCharsets
import java.nio.file.Path
import kotlin.io.path.*
class AstCodegen(val pkg: String, val outputDirectory: Path, val world: AstWorld) {
private fun deleteAllContents() {
for (child in outputDirectory.listDirectoryEntries("*.kt")) {
child.deleteExisting()
}
}
fun generate() {
deleteAllContents()
for (type in world.typeRegistry.types) {
writeAstType(type)
}
writeNodeType()
writeNodeVisitor()
writeNodeCoalescer()
}
private fun writeNodeType() {
val enumClass = KotlinEnum(pkg, "NodeType")
val parentMember = KotlinMember("parent", "NodeType?", value = "null")
enumClass.members.add(parentMember)
for (type in world.typesInDependencyOrder()) {
val role = world.typeRegistry.roleOfType(type)
if (role == AstTypeRole.ValueHolder || role == AstTypeRole.Enum) {
continue
}
val entry = KotlinEnumEntry(type.name)
if (type.parent != null) {
entry.parameters.add(type.parent!!.name)
}
enumClass.entries.add(entry)
}
write("NodeType.kt", KotlinWriter(enumClass))
}
private fun writeNodeVisitor() {
val visitorInterface = KotlinClass(
pkg,
"NodeVisitor",
typeParameters = mutableListOf("T"),
isInterface = true
)
for (type in world.typesInDependencyOrder()) {
val role = world.typeRegistry.roleOfType(type)
if (role != AstTypeRole.AstNode) {
continue
}
val visitFunction = KotlinFunction(
"visit${type.name}",
returnType = "T",
parameters = mutableListOf(
KotlinParameter("node", type.name)
),
isInterfaceMethod = true
)
visitorInterface.functions.add(visitFunction)
}
val visitNodesFunction = KotlinFunction(
"visitNodes",
returnType = "List<T>",
parameters = mutableListOf(
KotlinParameter("nodes", type = "Node?", vararg = true)
),
isImmediateExpression = true
)
visitNodesFunction.body.add("nodes.asSequence().filterNotNull().map { visit(it) }.toList()")
visitorInterface.functions.add(visitNodesFunction)
val visitAllFunction = KotlinFunction(
"visitAll",
returnType = "List<T>",
parameters = mutableListOf(
KotlinParameter("nodeLists", type = "List<Node>", vararg = true)
),
isImmediateExpression = true
)
visitAllFunction.body.add("nodeLists.asSequence().flatten().map { visit(it) }.toList()")
visitorInterface.functions.add(visitAllFunction)
val visitFunction = KotlinFunction(
"visit",
returnType = "T",
parameters = mutableListOf(
KotlinParameter("node", type = "Node")
),
isImmediateExpression = true
)
visitFunction.body.add("when (node) {")
for (type in world.typeRegistry.types.filter { world.typeRegistry.roleOfType(it) == AstTypeRole.AstNode }) {
visitFunction.body.add(" is ${type.name} -> visit${type.name}(node)")
}
visitFunction.body.add("}")
visitorInterface.functions.add(visitFunction)
write("NodeVisitor.kt", KotlinWriter(visitorInterface))
}
private fun writeNodeCoalescer() {
val coalescerClass = KotlinClass(
pkg,
"NodeCoalescer",
inherits = mutableListOf("NodeVisitor<Unit>"),
members = mutableListOf(
KotlinMember(
"handler",
"(Node) -> Unit"
)
)
)
for (type in world.typesInDependencyOrder()) {
val role = world.typeRegistry.roleOfType(type)
if (role != AstTypeRole.AstNode) {
continue
}
val function = KotlinFunction(
"visit${type.name}",
returnType = "Unit",
parameters = mutableListOf(
KotlinParameter("node", type.name)
),
isImmediateExpression = true,
overridden = true
)
function.body.add("handle(node)")
coalescerClass.functions.add(function)
}
val handleFunction = KotlinFunction(
"handle",
parameters = mutableListOf(
KotlinParameter("node", "Node")
)
)
handleFunction.body.add("handler(node)")
handleFunction.body.add("node.visitChildren(this)")
coalescerClass.functions.add(handleFunction)
write("NodeCoalescer.kt", KotlinWriter(coalescerClass))
}
private fun writeAstType(type: AstType) {
val role = world.typeRegistry.roleOfType(type)
val kotlinClassLike: KotlinClassLike
if (role == AstTypeRole.Enum) {
val kotlinEnum = KotlinEnum(pkg, type.name)
kotlinClassLike = kotlinEnum
} else {
val kotlinClass = KotlinClass(pkg, type.name)
kotlinClassLike = kotlinClass
if (role == AstTypeRole.RootNode || role == AstTypeRole.HierarchyNode) {
kotlinClass.sealed = true
}
}
if (role == AstTypeRole.RootNode) {
val typeMember = KotlinMember(
"type",
"NodeType",
abstract = true
)
kotlinClassLike.members.add(typeMember)
val abstractVisitChildrenFunction = KotlinFunction(
"visitChildren",
returnType = "List<T>",
open = true,
typeParameters = mutableListOf("T"),
parameters = mutableListOf(
KotlinParameter("visitor", "NodeVisitor<T>")
),
isImmediateExpression = true
)
abstractVisitChildrenFunction.body.add("emptyList()")
kotlinClassLike.functions.add(abstractVisitChildrenFunction)
} else if (role == AstTypeRole.AstNode) {
val typeMember = KotlinMember(
"type",
"NodeType",
overridden = true,
value = "NodeType.${type.name}"
)
kotlinClassLike.members.add(typeMember)
}
if (type.parent != null) {
val parentName = type.parent!!.name
kotlinClassLike.inherits.add("$parentName()")
}
for (value in type.values) {
val member = KotlinMember(value.name, toKotlinType(value.typeRef))
member.abstract = value.abstract
if (type.isParentAbstract(value)) {
member.overridden = true
}
if (role == AstTypeRole.ValueHolder) {
member.mutable = true
}
kotlinClassLike.members.add(member)
}
if (role == AstTypeRole.Enum) {
val kotlinEnum = kotlinClassLike as KotlinEnum
for (entry in type.enums) {
val orderOfKeys = entry.values.keys.sortedBy { key ->
kotlinClassLike.members.indexOfFirst { it.name == key }
}
val parameters = mutableListOf<String>()
for (key in orderOfKeys) {
val value = entry.values[key] ?: continue
parameters.add("\"${value}\"")
}
val enumEntry = KotlinEnumEntry(entry.name, parameters)
kotlinEnum.entries.add(enumEntry)
}
}
if (role == AstTypeRole.AstNode) {
val visitChildrenFunction = KotlinFunction(
"visitChildren",
returnType = "List<T>",
typeParameters = mutableListOf("T")
)
visitChildrenFunction.overridden = true
val visitorParameter = KotlinParameter("visitor", "NodeVisitor<T>")
visitChildrenFunction.parameters.add(visitorParameter)
visitChildrenFunction.isImmediateExpression = true
val anyListMembers = type.values.any { it.typeRef.form == AstTypeRefForm.List }
val elideVisitChildren: Boolean
if (anyListMembers) {
val visitParameters = type.values.mapNotNull {
if (it.typeRef.primitive != null) {
null
} else if (it.typeRef.type != null && !world.typeRegistry.roleOfType(it.typeRef.type).isNodeInherited()) {
null
} else if (it.typeRef.form == AstTypeRefForm.Single) {
"listOf(${it.name})"
} else {
it.name
}
}.joinToString(", ")
elideVisitChildren = visitParameters.isEmpty()
visitChildrenFunction.body.add("visitor.visitAll(${visitParameters})")
} else {
val visitParameters = type.values.mapNotNull {
if (it.typeRef.primitive != null) {
null
} else if (it.typeRef.type != null && !world.typeRegistry.roleOfType(it.typeRef.type).isNodeInherited()) {
null
} else {
it.name
}
}.joinToString(", ")
elideVisitChildren = visitParameters.isEmpty()
visitChildrenFunction.body.add("visitor.visitNodes(${visitParameters})")
}
if (!elideVisitChildren) {
kotlinClassLike.functions.add(visitChildrenFunction)
}
val equalsAndHashCodeMembers = kotlinClassLike.members.map { it.name }.sortedBy { it == "type" }
val equalsFunction = KotlinFunction(
"equals",
returnType = "Boolean",
overridden = true
)
equalsFunction.parameters.add(KotlinParameter(
"other",
"Any?"
))
equalsFunction.body.add("if (other !is ${type.name}) return false")
val predicate = equalsAndHashCodeMembers.mapNotNull {
if (it == "type") null else "other.${it} == $it"
}.joinToString(" && ")
equalsFunction.body.add("return $predicate")
kotlinClassLike.functions.add(equalsFunction)
val hashCodeFunction = KotlinFunction(
"hashCode",
returnType = "Int",
overridden = true
)
if (equalsAndHashCodeMembers.size == 1) {
val member = equalsAndHashCodeMembers.single()
hashCodeFunction.isImmediateExpression = true
hashCodeFunction.body.add("31 * ${member}.hashCode()")
} else {
for ((index, value) in equalsAndHashCodeMembers.withIndex()) {
if (index == 0) {
hashCodeFunction.body.add("var result = ${value}.hashCode()")
} else {
hashCodeFunction.body.add("result = 31 * result + ${value}.hashCode()")
}
}
hashCodeFunction.body.add("return result")
}
kotlinClassLike.functions.add(hashCodeFunction)
}
val serialName = kotlinClassLike.name[0].lowercase() +
kotlinClassLike.name.substring(1)
kotlinClassLike.imports.add("kotlinx.serialization.SerialName")
kotlinClassLike.imports.add("kotlinx.serialization.Serializable")
kotlinClassLike.annotations.add("Serializable")
kotlinClassLike.annotations.add("SerialName(\"$serialName\")")
write("${type.name}.kt", KotlinWriter(kotlinClassLike))
}
private fun toKotlinType(typeRef: AstTypeRef): String {
val baseType = typeRef.type?.name ?: typeRef.primitive?.id
?: throw RuntimeException("Unable to determine base type.")
return when (typeRef.form) {
AstTypeRefForm.Single -> baseType
AstTypeRefForm.Nullable -> "${baseType}?"
AstTypeRefForm.List -> "List<${baseType}>"
}
}
private fun write(fileName: String, writer: KotlinWriter) {
val path = outputDirectory.resolve(fileName)
path.deleteIfExists()
path.writeText(writer.toString(), StandardCharsets.UTF_8)
}
companion object {
fun run(pkg: String, astDescriptionFile: Path, outputDirectory: Path) {
if (!outputDirectory.exists()) {
outputDirectory.createDirectories()
}
val astYamlText = astDescriptionFile.readText()
val mapper = ObjectMapper(YAMLFactory())
mapper.registerModules(KotlinModule.Builder().build())
val astDescription = mapper.readValue(astYamlText, AstDescription::class.java)
val world = AstWorld.build(astDescription)
val codegen = AstCodegen(pkg, outputDirectory, world)
codegen.generate()
}
}
}

View File

@ -0,0 +1,6 @@
package gay.pizza.pork.buildext.ast
data class AstDescription(
val root: String,
val types: Map<String, AstTypeDescription>
)

View File

@ -0,0 +1,6 @@
package gay.pizza.pork.buildext.ast
class AstEnum(
val name: String,
val values: Map<String, String>
)

View File

@ -0,0 +1,6 @@
package gay.pizza.pork.buildext.ast
class AstEnumDescription(
val name: String,
val values: Map<String, String>
)

View File

@ -0,0 +1,7 @@
package gay.pizza.pork.buildext.ast
enum class AstPrimitive(val id: kotlin.String) {
Boolean("Boolean"),
String("String"),
Int("Int")
}

View File

@ -0,0 +1,40 @@
package gay.pizza.pork.buildext.ast
class AstType(val name: String, var parent: AstType? = null) {
private val internalValues = mutableListOf<AstValue>()
private val internalEnums = mutableListOf<AstEnum>()
val values: List<AstValue>
get() = internalValues
val enums: List<AstEnum>
get() = internalEnums
internal fun addValue(value: AstValue) {
internalValues.add(value)
}
internal fun addEnum(enum: AstEnum) {
internalEnums.add(enum)
}
fun isParentAbstract(value: AstValue): Boolean {
if (parent == null) {
return false
}
var current = parent
while (current != null) {
val abstract = current.values.firstOrNull {
it.name == value.name && it.abstract
}
if (abstract != null) {
return true
}
current = current.parent
}
return false
}
override fun toString(): String = "AstType(${name})"
}

View File

@ -0,0 +1,7 @@
package gay.pizza.pork.buildext.ast
data class AstTypeDescription(
val parent: String? = null,
val values: List<AstValueDescription> = emptyList(),
val enums: List<AstEnumDescription> = emptyList()
)

View File

@ -0,0 +1,38 @@
package gay.pizza.pork.buildext.ast
class AstTypeRef(
val type: AstType? = null,
val primitive: AstPrimitive? = null,
val form: AstTypeRefForm
) {
companion object {
fun parse(input: String, registry: AstTypeRegistry): AstTypeRef {
if (input.startsWith("List<")) {
val underlyingType = input.substring(5, input.length - 1)
val underlyingRef = parse(underlyingType, registry)
return AstTypeRef(
type = underlyingRef.type,
primitive = underlyingRef.primitive,
form = AstTypeRefForm.List
)
}
var form = AstTypeRefForm.Single
var typeName: String = input
if (input.endsWith("?")) {
form = AstTypeRefForm.Nullable
typeName = input.substring(0, input.length - 1)
}
val primitive = AstPrimitive.values().firstOrNull { it.name == typeName }
if (primitive != null) {
return AstTypeRef(
primitive = primitive,
form = form
)
}
return AstTypeRef(type = registry.lookup(typeName), form = form)
}
}
}

View File

@ -0,0 +1,7 @@
package gay.pizza.pork.buildext.ast
enum class AstTypeRefForm {
Single,
List,
Nullable
}

View File

@ -0,0 +1,34 @@
package gay.pizza.pork.buildext.ast
class AstTypeRegistry {
private val internalTypes = mutableSetOf<AstType>()
val types: Set<AstType>
get() = internalTypes
fun add(type: AstType): AstType {
internalTypes.add(type)
return type
}
fun lookupOrNull(name: String): AstType? =
internalTypes.singleOrNull { it.name == name }
fun lookup(name: String): AstType = lookupOrNull(name)
?: throw RuntimeException("Unknown AstType: $name")
fun roleOfType(type: AstType): AstTypeRole =
when {
type.enums.isNotEmpty() ->
AstTypeRole.Enum
type.parent == null && type.values.isEmpty() ->
AstTypeRole.RootNode
type.parent != null && type.values.all { it.abstract } ->
AstTypeRole.HierarchyNode
type.parent != null && type.values.none { it.abstract } ->
AstTypeRole.AstNode
type.parent == null && type.values.isNotEmpty() ->
AstTypeRole.ValueHolder
else -> throw RuntimeException("Unable to determine role of type ${type.name}")
}
}

View File

@ -0,0 +1,16 @@
package gay.pizza.pork.buildext.ast
enum class AstTypeRole {
RootNode,
HierarchyNode,
AstNode,
ValueHolder,
Enum;
fun isNodeInherited(): Boolean = when (this) {
RootNode -> true
HierarchyNode -> true
AstNode -> true
else -> false
}
}

View File

@ -0,0 +1,7 @@
package gay.pizza.pork.buildext.ast
class AstValue(
val name: String,
val typeRef: AstTypeRef,
val abstract: Boolean = false
)

View File

@ -0,0 +1,7 @@
package gay.pizza.pork.buildext.ast
data class AstValueDescription(
val name: String,
val type: String,
val required: Boolean = false
)

View File

@ -0,0 +1,56 @@
package gay.pizza.pork.buildext.ast
class AstWorld {
val typeRegistry: AstTypeRegistry = AstTypeRegistry()
fun typesInDependencyOrder(): List<AstType> {
val typesInNameOrder = typeRegistry.types.sortedBy { it.name }
val typesInDependencyOrder = mutableListOf<AstType>()
for (type in typesInNameOrder) {
if (type.parent != null) {
if (!typesInDependencyOrder.contains(type.parent)) {
typesInDependencyOrder.add(type.parent!!)
}
}
if (!typesInDependencyOrder.contains(type)) {
typesInDependencyOrder.add(type)
}
}
return typesInDependencyOrder
}
companion object {
fun build(description: AstDescription): AstWorld {
val world = AstWorld()
val rootType = world.typeRegistry.add(AstType(description.root))
for (typeName in description.types.keys) {
if (typeName == rootType.name) {
throw RuntimeException("Cannot have type with the same name as the root type.")
}
world.typeRegistry.add(AstType(typeName))
}
for ((typeName, typeDescription) in description.types) {
val type = world.typeRegistry.lookup(typeName)
if (typeDescription.parent != null) {
type.parent = world.typeRegistry.lookup(typeDescription.parent)
}
for (value in typeDescription.values) {
val typeRef = AstTypeRef.parse(value.type, world.typeRegistry)
val typeValue = AstValue(value.name, typeRef, abstract = value.required)
type.addValue(typeValue)
}
for (enum in typeDescription.enums) {
val astEnum = AstEnum(enum.name, enum.values)
type.addEnum(astEnum)
}
}
return world
}
}
}

View File

@ -0,0 +1,14 @@
package gay.pizza.pork.buildext.ast
import kotlin.io.path.Path
object RunCodegenIde {
@JvmStatic
fun main(args: Array<String>) {
AstCodegen.run(
pkg = "gay.pizza.pork.ast",
astDescriptionFile = Path("src/main/ast/pork.yml"),
outputDirectory = Path("src/main/kotlin/gay/pizza/pork/ast")
)
}
}

View File

@ -0,0 +1,14 @@
package gay.pizza.pork.buildext.codegen
class KotlinClass(
override val pkg: String,
override var name: String,
var sealed: Boolean = false,
var isInterface: Boolean = false,
override var imports: MutableList<String> = mutableListOf(),
override var annotations: MutableList<String> = mutableListOf(),
override var typeParameters: MutableList<String> = mutableListOf(),
override var inherits: MutableList<String> = mutableListOf(),
override var members: MutableList<KotlinMember> = mutableListOf(),
override var functions: MutableList<KotlinFunction> = mutableListOf()
) : KotlinClassLike()

View File

@ -0,0 +1,12 @@
package gay.pizza.pork.buildext.codegen
abstract class KotlinClassLike {
abstract val pkg: String
abstract val name: String
abstract var imports: MutableList<String>
abstract var annotations: MutableList<String>
abstract var typeParameters: MutableList<String>
abstract var inherits: MutableList<String>
abstract var members: MutableList<KotlinMember>
abstract var functions: MutableList<KotlinFunction>
}

View File

@ -0,0 +1,13 @@
package gay.pizza.pork.buildext.codegen
class KotlinEnum(
override val pkg: String,
override val name: String,
override var imports: MutableList<String> = mutableListOf(),
override var annotations: MutableList<String> = mutableListOf(),
override var typeParameters: MutableList<String> = mutableListOf(),
override var inherits: MutableList<String> = mutableListOf(),
override var members: MutableList<KotlinMember> = mutableListOf(),
override var functions: MutableList<KotlinFunction> = mutableListOf(),
var entries: MutableList<KotlinEnumEntry> = mutableListOf()
) : KotlinClassLike()

View File

@ -0,0 +1,6 @@
package gay.pizza.pork.buildext.codegen
class KotlinEnumEntry(
val name: String,
var parameters: MutableList<String> = mutableListOf()
)

View File

@ -0,0 +1,14 @@
package gay.pizza.pork.buildext.codegen
class KotlinFunction(
val name: String,
var typeParameters: MutableList<String> = mutableListOf(),
var parameters: MutableList<KotlinParameter> = mutableListOf(),
var returnType: String? = null,
var abstract: Boolean = false,
var open: Boolean = false,
var overridden: Boolean = false,
var isImmediateExpression: Boolean = false,
var body: MutableList<String> = mutableListOf(),
var isInterfaceMethod: Boolean = false
)

View File

@ -0,0 +1,10 @@
package gay.pizza.pork.buildext.codegen
class KotlinMember(
var name: String,
var type: String,
var abstract: Boolean = false,
var overridden: Boolean = false,
var value: String? = null,
var mutable: Boolean = false
)

View File

@ -0,0 +1,8 @@
package gay.pizza.pork.buildext.codegen
class KotlinParameter(
val name: String,
var type: String,
var defaultValue: String? = null,
var vararg: Boolean = false
)

View File

@ -0,0 +1,214 @@
package gay.pizza.pork.buildext.codegen
class KotlinWriter() {
private val buffer = StringBuilder()
constructor(kotlinClassLike: KotlinClassLike) : this() {
write(kotlinClassLike)
}
fun writeClass(kotlinClass: KotlinClass): Unit = buffer.run {
val classType = when {
kotlinClass.sealed -> "sealed class"
kotlinClass.isInterface -> "interface"
else -> "class"
}
writeClassLike(classType, kotlinClass)
val members = kotlinClass.members.filter {
it.abstract || (it.overridden && it.value != null)
}
if (members.isEmpty() && kotlinClass.functions.isEmpty()) {
appendLine()
} else {
appendLine(" {")
}
for (member in members) {
val form = if (member.mutable) "var" else "val"
if (member.abstract) {
appendLine(" abstract $form ${member.name}: ${member.type}")
} else {
if (member.overridden) {
append(" override ")
}
append("$form ${member.name}: ${member.type}")
if (member.value != null) {
append(" = ")
append(member.value)
}
appendLine()
}
}
if (members.isNotEmpty() && kotlinClass.functions.isNotEmpty()) {
appendLine()
}
writeFunctions(kotlinClass)
if (members.isNotEmpty() || kotlinClass.functions.isNotEmpty()) {
appendLine("}")
}
}
fun writeEnum(kotlinEnum: KotlinEnum): Unit = buffer.run {
writeClassLike("enum class", kotlinEnum)
val membersNotCompatible = kotlinEnum.members.filter { it.abstract }
if (membersNotCompatible.isNotEmpty()) {
throw RuntimeException(
"Incompatible members in enum class " +
"${kotlinEnum.name}: $membersNotCompatible"
)
}
if (kotlinEnum.entries.isEmpty() && kotlinEnum.functions.isEmpty()) {
appendLine()
} else {
appendLine(" {")
}
for ((index, entry) in kotlinEnum.entries.withIndex()) {
append(" ${entry.name}")
if (entry.parameters.isNotEmpty()) {
append("(")
append(entry.parameters.joinToString(", "))
append(")")
}
if (index != kotlinEnum.entries.size - 1) {
append(",")
}
appendLine()
}
if (kotlinEnum.entries.isNotEmpty() && kotlinEnum.functions.isNotEmpty()) {
appendLine()
}
writeFunctions(kotlinEnum)
if (kotlinEnum.entries.isNotEmpty()) {
appendLine("}")
}
}
private fun writeClassLike(
classType: String,
kotlinClass: KotlinClassLike
): Unit = buffer.run {
appendLine("package ${kotlinClass.pkg}")
appendLine()
for (import in kotlinClass.imports) {
appendLine("import $import")
}
if (kotlinClass.imports.isNotEmpty()) {
appendLine()
}
for (annotation in kotlinClass.annotations) {
appendLine("@${annotation}")
}
append("$classType ${kotlinClass.name}")
if (kotlinClass.typeParameters.isNotEmpty()) {
val typeParameters = kotlinClass.typeParameters.joinToString(", ")
append("<${typeParameters}>")
}
val contructedMembers = kotlinClass.members.filter {
!it.abstract && !(it.overridden && it.value != null)
}
if (contructedMembers.isNotEmpty()) {
val constructor = contructedMembers.joinToString(", ") {
val prefix = if (it.overridden) "override " else ""
val form = if (it.mutable) "var" else "val"
val start = "${prefix}$form ${it.name}: ${it.type}"
if (it.value != null) {
"$start = ${it.value}"
} else start
}
append("(${constructor})")
}
if (kotlinClass.inherits.isNotEmpty()) {
append(" : ${kotlinClass.inherits.joinToString(", ")}")
}
}
private fun writeFunctions(kotlinClassLike: KotlinClassLike): Unit = buffer.run {
for ((index, function) in kotlinClassLike.functions.withIndex()) {
append(" ")
if (function.overridden) {
append("override ")
}
if (function.abstract) {
append("abstract ")
}
if (function.open) {
append("open ")
}
append("fun ")
if (function.typeParameters.isNotEmpty()) {
append("<${function.typeParameters.joinToString(", ")}> ")
}
append("${function.name}(")
append(function.parameters.joinToString(", ") {
var start = "${it.name}: ${it.type}"
if (it.vararg) {
start = "vararg $start"
}
if (it.defaultValue != null) {
start + " = ${it.defaultValue}"
} else start
})
append(")")
if (function.returnType != null) {
append(": ${function.returnType}")
}
if (!function.isImmediateExpression && !function.abstract && !function.isInterfaceMethod) {
append(" {")
} else if (!function.abstract && !function.isInterfaceMethod) {
append(" =")
}
if (function.body.isNotEmpty()) {
appendLine()
for (item in function.body) {
appendLine(" $item")
}
}
if (!function.isImmediateExpression && !function.abstract && !function.isInterfaceMethod) {
if (function.body.isNotEmpty()) {
append(" ")
}
appendLine("}")
}
if (function.abstract || function.isInterfaceMethod) {
appendLine()
}
if (index < kotlinClassLike.functions.size - 1) {
appendLine()
}
}
}
fun write(input: KotlinClassLike): Unit = when (input) {
is KotlinClass -> writeClass(input)
is KotlinEnum -> writeEnum(input)
else -> throw RuntimeException("Unknown Kotlin Class Type")
}
override fun toString(): String = buffer.toString()
}