Very basic framework for codegen

This commit is contained in:
Johannes Frohnmeyer 2022-10-13 19:12:11 +02:00
parent 4ddd8fe9de
commit aa0209d7de
Signed by: Johannes
GPG Key ID: E76429612C2929F4
5 changed files with 248 additions and 0 deletions

View File

@ -0,0 +1,18 @@
package io.gitlab.jfronny.scripts
import io.gitlab.jfronny.scripts.codegen.ContentGenerator
import org.gradle.api.Action
import org.gradle.api.Project
import org.gradle.api.tasks.SourceSet
import org.gradle.kotlin.dsl.extra
val Project.codegenDir get() = buildDir.resolve("generated/sources/jfCodegen")
fun SourceSet.generate(project: Project, generate: Action<ContentGenerator>) {
val generators = project.extra["codeGenerators"] as LinkedHashMap<String, ContentGenerator.Generated>
val generator = ContentGenerator()
generate.execute(generator)
generators[name] = generator.finalize()
java.srcDir(project.codegenDir.resolve("java/$name"))
resources.srcDir(project.codegenDir.resolve("resources/$name"))
}

View File

@ -0,0 +1,127 @@
package io.gitlab.jfronny.scripts.codegen
class ClassGenerator(`package`: String, val name: String, val indent: String = " ") : Generator<String>() {
override fun generateFinalized() = ensureBody { gen.appendLine("}").toString() }
private val gen: StringBuilder = StringBuilder("package $`package`;\n\n")
private var headerGenerated = false
private var extends: String? = null
private val implements: MutableList<String> = ArrayList()
private var hadImport = false
fun <T> ensureHeader(action: () -> T): T = ensureMutable {
if (headerGenerated) throw IllegalAccessException("Attempted to generate pre-header statement while in body")
action()
}
fun <T> ensureBody(action: () -> T): T = ensureMutable {
if (!headerGenerated) {
headerGenerated = true
if (hadImport) gen.appendLine()
gen.append("public class $name")
if (extends != null) gen.append(" extends $extends")
if (implements.isNotEmpty()) gen.append(" implements ").append(implements.joinToString(", ") { it })
gen.append(" {")
gen.appendLine()
}
action()
}
fun rawHead(toWrite: String): Unit = ensureHeader { gen.appendLine(toWrite.trimIndent().trim('\n').prependIndent(indent)) }
fun rawBody(toWrite: String): Unit = ensureBody { gen.appendLine(toWrite.trimIndent().trim('\n').prependIndent(indent)) }
fun import(`import`: String): Unit = ensureHeader {
check(import, "import", importPattern)
gen.appendLine("import $import;")
hadImport = true
}
fun extends(extends: String): Unit = ensureHeader {
check(extends, "extended class", classPattern)
if (this.extends != null) throw IllegalAccessException("Attempted to extend multiple classes")
this.extends = extends
}
fun implements(implements: String): Unit = ensureHeader {
check(implements, "implemented interface", classPattern)
this.implements.add(implements)
}
fun fieldRaw(name: String, type: String, valueRaw: String, modifiers: String = defaultModifiers): Unit = ensureBody {
check(name, "field", classEntryPattern)
check(type, "field type", classPattern)
check(modifiers, "set of modifiers", modifierPattern)
gen.appendLine("$indent$modifiers $type $name = $valueRaw;")
}
fun field(name: String, value: Int, modifiers: String = defaultModifiers) = fieldRaw(name, "int", value.toString(), modifiers)
fun field(name: String, value: Boolean, modifiers: String = defaultModifiers) = fieldRaw(name, "boolean", value.toString(), modifiers)
fun field(name: String, value: Double, modifiers: String = defaultModifiers) = fieldRaw(name, "double", value.toString(), modifiers)
fun field(name: String, value: Float, modifiers: String = defaultModifiers) = fieldRaw(name, "float", value.toString() + "f", modifiers)
fun field(name: String, value: String, modifiers: String = defaultModifiers) = fieldRaw(name, "String", enquote(value), modifiers)
fun procedure(name: String, argument: Map<String, String>, modifiers: String = defaultModifiers, body: String, throws: String? = null): Unit = ensureBody {
check(name, "procedure", classEntryPattern)
if (argument.any { !classEntryPattern.matches(it.key) || !classPattern.matches(it.value) }) throw IllegalArgumentException("invalid argument on procedure")
check(modifiers, "set of modifiers", modifierPattern)
ensureBrackets(body)
if (throws != null) check(throws, "thrown exceptions list", classListPattern)
gen.append(indent)
gen.append(modifiers)
gen.append(" void ")
gen.append(name)
gen.append("(")
gen.append(argument.entries.joinToString(", ") { "${it.key} ${it.value}" })
gen.append(") ")
if (throws != null) gen.append(throws).append(' ')
gen.append('{')
gen.appendLine()
gen.appendLine(body.trimIndent().prependIndent("$indent "))
gen.appendLine("$indent}")
}
fun function(name: String, returnType: String, argument: Map<String, String>, modifiers: String = defaultModifiers, body: String, throws: String? = null): Unit = ensureBody {
check(name, "procedure", classEntryPattern)
check(returnType, "return type", classPattern)
if (argument.any { !classEntryPattern.matches(it.key) || !classPattern.matches(it.value) }) throw IllegalArgumentException("invalid argument on procedure")
check(modifiers, "set of modifiers", modifierPattern)
ensureBrackets(body)
if (throws != null) check(throws, "thrown exceptions list", classListPattern)
gen.append(indent)
gen.append(modifiers)
gen.append(" $returnType ")
gen.append(name)
gen.append("(")
gen.append(argument.entries.joinToString(", ") { "${it.key} ${it.value}" })
gen.append(") ")
if (throws != null) gen.append(throws).append(' ')
gen.append('{')
gen.appendLine()
gen.appendLine(body.trimIndent().prependIndent("$indent "))
gen.appendLine("$indent}")
}
companion object {
@JvmStatic private val defaultModifiers = "public static"
@JvmStatic private fun enquote(value: String): String = "\"${value.replace("\\", "\\\\").replace("\"", "\\\"")}\""
@JvmStatic private fun ensureBrackets(value: String): Unit {
var state = 0 // 0 -> outside of anything, 1 -> inside string, 2 -> inside block quote
var brackets = 0
value.forEachIndexed { i, c ->
when (state) {
0 -> {
if (c == '"') state = if (i + 2 < value.length && value[i+1] == '"' && value[i+2] == '"') 2 else 1
if (c == '{') brackets++
if (c == '}') brackets--
}
1 -> {
if (c == '"') state = 0
}
2 -> {
if (c == '"' && i + 2 < value.length && value[i+1] == '"' && value[i+2] == '"') state = 0
}
}
}
if (brackets != 0) throw IllegalArgumentException("Unclosed brackets in method body")
}
}
}

View File

@ -0,0 +1,32 @@
package io.gitlab.jfronny.scripts.codegen
import org.gradle.api.Action
import java.util.function.Supplier
class ContentGenerator : Generator<ContentGenerator.Generated>() {
private val classes: MutableMap<String, String> = LinkedHashMap()
private val resources: MutableMap<String, ByteArray> = LinkedHashMap()
override fun generateFinalized() = Generated(ImmutableMap(classes), ImmutableMap(resources))
data class Generated(val classes: Map<String, String>, val resources: Map<String, ByteArray>)
fun `class`(`package`: String, name: String, generator: Action<ClassGenerator>) = ensureMutable {
if (!packagePattern.matches(`package`)) throw IllegalArgumentException("package \"$`package`\" is not a valid package")
if (!classNamePattern.matches(name)) throw IllegalArgumentException("class name \"$name\" is not a valid class name")
val builder = ClassGenerator(`package`, name)
generator.execute(builder)
classes["${`package`.replace('.', '/')}/$name.java"] = builder.finalize()
}
fun text(path: String, generator: Action<StringBuilder>) = ensureMutable {
if (!pathPattern.matches(path)) throw IllegalArgumentException("path \"$path\" is not a valid path")
val builder = StringBuilder()
generator.execute(builder)
resources[path] = builder.toString().toByteArray()
}
fun blob(path: String, generator: Supplier<ByteArray>) = ensureMutable {
if (!pathPattern.matches(path)) throw IllegalArgumentException("path \"$path\" is not a valid path")
resources[path] = generator.get()
}
}

View File

@ -0,0 +1,36 @@
package io.gitlab.jfronny.scripts.codegen
abstract class Generator<T> {
private var finalized = false
protected abstract fun generateFinalized(): T
fun finalize(): T {
val fin = generateFinalized()
finalized = true
return fin
}
protected fun <T> ensureMutable(action: () -> T): T {
if (finalized) throw IllegalAccessException("Attempted to access CodeGenerator after it was finalized")
return action()
}
protected data class ImmutableMap<Key, Value>(private val inner: Map<Key, Value>): Map<Key, Value> by inner
companion object {
@JvmStatic protected val primitivePattern = Regex("(?:byte|short|int|long|float|double|boolean|char)")
@JvmStatic protected val singleModifierPattern = Regex("(?:public|private|protected|static|final|abstract|transient|synchronized|volatile)")
@JvmStatic protected val modifierPattern = Regex("(?:$singleModifierPattern(?: $singleModifierPattern)*)")
@JvmStatic protected val packagePattern = Regex("(?:[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*)")
@JvmStatic protected val classNamePattern = Regex("(?:[A-Z][A-Za-z0-9_\$]*)")
@JvmStatic protected val pathPattern = Regex("(?:([a-z]+/)*[a-zA-Z][a-zA-Z0-9_.]*)")
@JvmStatic protected val classPattern = Regex("(?:(?:(?:$packagePattern\\.)?$classNamePattern)|$primitivePattern)")
@JvmStatic protected val importPattern = Regex("(?:$packagePattern\\.(?:\\*|$classNamePattern))")
@JvmStatic protected val classEntryPattern = Regex("(?:[a-zA-Z][a-zA-Z_\$0-9]*)")
@JvmStatic protected val classListPattern = Regex("(?:$classPattern(?:, $classPattern)*)")
@JvmStatic protected fun check(name: String, type: String, pattern: Regex): Unit {
if (!pattern.matches(name)) throw IllegalArgumentException("$type \"$name\" is not a valid $type")
}
}
}

View File

@ -0,0 +1,35 @@
import gradle.kotlin.dsl.accessors._72efc76fad8c8cf3476d335fb6323bde.*
import io.gitlab.jfronny.scripts.codegen.ContentGenerator.Generated
import io.gitlab.jfronny.scripts.*
plugins {
java
}
extra["codeGenerators"] = LinkedHashMap<String, Generated>()
val jfCodegen by tasks.registering {
doLast {
(project.extra["codeGenerators"] as LinkedHashMap<String, Generated>).forEach { (name, generated) ->
generated.classes.forEach { (filePath, content) ->
val path = codegenDir.resolve("java").resolve(name).resolve(filePath)
path.parentFile.mkdirs()
path.writeText(content)
}
generated.resources.forEach { (filePath, content) ->
val path = codegenDir.resolve("resources").resolve(name).resolve(filePath)
path.parentFile.mkdirs()
path.writeBytes(content)
}
}
project.extra["codeGenerators"] = null
}
}
tasks.compileJava {
dependsOn(jfCodegen.get())
}
tasks.processResources {
dependsOn(jfCodegen.get())
}