mirror of
https://github.com/GayPizzaSpecifications/pork.git
synced 2025-08-03 13:11:32 +00:00
Auto-generate the AST.
This commit is contained in:
37
buildext/build.gradle.kts
Normal file
37
buildext/build.gradle.kts
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package gay.pizza.pork.buildext.ast
|
||||
|
||||
data class AstDescription(
|
||||
val root: String,
|
||||
val types: Map<String, AstTypeDescription>
|
||||
)
|
@ -0,0 +1,6 @@
|
||||
package gay.pizza.pork.buildext.ast
|
||||
|
||||
class AstEnum(
|
||||
val name: String,
|
||||
val values: Map<String, String>
|
||||
)
|
@ -0,0 +1,6 @@
|
||||
package gay.pizza.pork.buildext.ast
|
||||
|
||||
class AstEnumDescription(
|
||||
val name: String,
|
||||
val values: Map<String, String>
|
||||
)
|
@ -0,0 +1,7 @@
|
||||
package gay.pizza.pork.buildext.ast
|
||||
|
||||
enum class AstPrimitive(val id: kotlin.String) {
|
||||
Boolean("Boolean"),
|
||||
String("String"),
|
||||
Int("Int")
|
||||
}
|
@ -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})"
|
||||
}
|
@ -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()
|
||||
)
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package gay.pizza.pork.buildext.ast
|
||||
|
||||
enum class AstTypeRefForm {
|
||||
Single,
|
||||
List,
|
||||
Nullable
|
||||
}
|
@ -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}")
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package gay.pizza.pork.buildext.ast
|
||||
|
||||
class AstValue(
|
||||
val name: String,
|
||||
val typeRef: AstTypeRef,
|
||||
val abstract: Boolean = false
|
||||
)
|
@ -0,0 +1,7 @@
|
||||
package gay.pizza.pork.buildext.ast
|
||||
|
||||
data class AstValueDescription(
|
||||
val name: String,
|
||||
val type: String,
|
||||
val required: Boolean = false
|
||||
)
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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")
|
||||
)
|
||||
}
|
||||
}
|
@ -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()
|
@ -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>
|
||||
}
|
@ -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()
|
@ -0,0 +1,6 @@
|
||||
package gay.pizza.pork.buildext.codegen
|
||||
|
||||
class KotlinEnumEntry(
|
||||
val name: String,
|
||||
var parameters: MutableList<String> = mutableListOf()
|
||||
)
|
@ -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
|
||||
)
|
@ -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
|
||||
)
|
@ -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
|
||||
)
|
@ -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()
|
||||
}
|
Reference in New Issue
Block a user