Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jvm field ir #1242

Merged
merged 4 commits into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ Thank you to all who have contributed!
## [Unreleased]

### Added
- Adds top-level IR node creation functions.
- Adds `componentN` functions (destructuring) to IR nodes via Kotlin data classes
- Adds public `tag` field to IR nodes for associating metadata

### Changed

Expand All @@ -36,12 +39,14 @@ Thank you to all who have contributed!
### Fixed

### Removed
- [Breaking] Removed IR factory in favor of static top-level functions. Change `Ast.foo()`
to `foo()`

### Security

### Contributors
Thank you to all who have contributed!
- @<your-username>
- @rchowell

## [0.13.2-alpha] - 2023-09-29

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@ package org.partiql.sprout.generator.target.kotlin

import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.asTypeName
import net.pearx.kasechange.toCamelCase
import org.partiql.sprout.generator.Generator
import org.partiql.sprout.generator.target.kotlin.poems.KotlinBuilderPoem
import org.partiql.sprout.generator.target.kotlin.poems.KotlinFactoryPoem
import org.partiql.sprout.generator.target.kotlin.poems.KotlinJacksonPoem
import org.partiql.sprout.generator.target.kotlin.poems.KotlinListenerPoem
import org.partiql.sprout.generator.target.kotlin.poems.KotlinUtilsPoem
Expand All @@ -21,14 +19,16 @@ import org.partiql.sprout.generator.target.kotlin.spec.KotlinNodeSpec
import org.partiql.sprout.generator.target.kotlin.spec.KotlinUniverseSpec
import org.partiql.sprout.model.TypeDef
import org.partiql.sprout.model.TypeProp
import org.partiql.sprout.model.TypeRef
import org.partiql.sprout.model.Universe

/**
* Generates and applies
*/
class KotlinGenerator(private val options: KotlinOptions) : Generator<KotlinResult> {

// @JvmField
private val jvmField = AnnotationSpec.builder(JvmField::class).build()

override fun generate(universe: Universe): KotlinResult {

// --- Initialize an empty symbol table(?)
Expand All @@ -39,6 +39,7 @@ class KotlinGenerator(private val options: KotlinOptions) : Generator<KotlinResu
val poems = options.poems.map {
when (it) {
"visitor" -> KotlinVisitorPoem(symbols)
"factory" -> KotlinFactoryPoem(symbols)
"builder" -> KotlinBuilderPoem(symbols)
"listener" -> KotlinListenerPoem(symbols)
"jackson" -> KotlinJacksonPoem(symbols)
Expand All @@ -51,12 +52,19 @@ class KotlinGenerator(private val options: KotlinOptions) : Generator<KotlinResu
val spec = KotlinUniverseSpec(
universe = universe,
nodes = universe.nodes(symbols),
base = TypeSpec.interfaceBuilder(symbols.base),
base = TypeSpec.classBuilder(symbols.base).addModifiers(KModifier.ABSTRACT),
types = universe.types(symbols)
)
val specs = with(spec) {
// Add identifiers
base.addProperty(PropertySpec.builder("_id", String::class).addModifiers(KModifier.ABSTRACT).build())
// Add optional tags
base.addProperty(
PropertySpec.builder("tag", String::class)
.addAnnotation(jvmField)
.mutable(true)
.initializer("\"${symbols.rootId}-\${%S.format(%T.nextInt())}\"", "%06x", ClassName("kotlin.random", "Random"))
.build()
)

// Apply each poem
poems.forEach { it.apply(this) }
// Finalize each spec/builder
Expand Down Expand Up @@ -85,7 +93,7 @@ class KotlinGenerator(private val options: KotlinOptions) : Generator<KotlinResu
*/
private fun Universe.nodes(symbols: KotlinSymbols): List<KotlinNodeSpec> =
types.mapNotNull { it.generate(symbols) }.map {
it.builder.addSuperinterface(symbols.base)
it.builder.superclass(symbols.base)
it
}

Expand All @@ -108,34 +116,41 @@ class KotlinGenerator(private val options: KotlinOptions) : Generator<KotlinResu
* Product Node Generation
*/
private fun TypeDef.Product.generate(symbols: KotlinSymbols): KotlinNodeSpec {
val clazz = symbols.clazz(ref)
val clazzImpl = ClassName(
packageName = clazz.packageName + ".impl",
simpleNames = listOf(symbols.pascal(ref) + "Impl"),
)
return KotlinNodeSpec.Product(
product = this,
props = props.map { KotlinNodeSpec.Prop(it.name.toCamelCase(), symbols.typeNameOf(it.ref)) },
implClazz = clazzImpl,
impl = TypeSpec.classBuilder(clazzImpl),
nodes = children.mapNotNull { it.generate(symbols) },
clazz = symbols.clazz(ref),
ext = (props.enumProps(symbols) + types.enums(symbols)).toMutableList(),
).apply {
// Add id to impl
impl.addProperty(symbols.idProp)
constructor.addParameter(symbols.idPara)

props.forEach {
val para = ParameterSpec.builder(it.name, it.type).build()
val prop = PropertySpec.builder(it.name, it.type).build()
builder.addProperty(prop.toBuilder().addModifiers(KModifier.ABSTRACT).build())
impl.addProperty(prop.toBuilder().addModifiers(KModifier.OVERRIDE).initializer(it.name).build())
val prop = PropertySpec.builder(it.name, it.type)
.initializer(it.name)
.addAnnotation(jvmField)
.build()
builder.addProperty(prop)
constructor.addParameter(para)
}

// Add `tag` field to all nodes

// HACK FOR EMPTY DATA CLASSES
if (props.isEmpty()) {
val name = " "
val type = Char::class
val para = ParameterSpec.builder(name, type).defaultValue("' '").build()
val prop = PropertySpec.builder(name, type)
.initializer("` `")
.addAnnotation(jvmField)
.build()
builder.addProperty(prop)
constructor.addParameter(para)
}
// impls are open
impl.superclass(clazz)
nodes.forEach { it.builder.addSuperinterface(symbols.base) }
this.addDataClassMethods(symbols, ref)

nodes.forEach { it.builder.superclass(symbols.base) }
builder.primaryConstructor(constructor.build())
}
}

Expand All @@ -149,16 +164,15 @@ class KotlinGenerator(private val options: KotlinOptions) : Generator<KotlinResu
clazz = symbols.clazz(ref),
ext = types.enums(symbols).toMutableList(),
).apply {
variants.forEach { it.builder.addSuperinterface(clazz) }
nodes.forEach { it.builder.addSuperinterface(symbols.base) }
variants.forEach { it.builder.superclass(clazz) }
nodes.forEach { it.builder.superclass(symbols.base) }
}

/**
* Enum constant generation
*/
private fun TypeDef.Enum.generate(symbols: KotlinSymbols) =
TypeSpec.enumBuilder(symbols.clazz(ref)).apply {
addFunction(enumToStringSpec(symbols.pascal(ref)))
values.forEach { addEnumConstant(it) }
}.build()

Expand All @@ -172,121 +186,4 @@ class KotlinGenerator(private val options: KotlinOptions) : Generator<KotlinResu
private fun List<TypeDef>.enums(symbols: KotlinSymbols) = filterIsInstance<TypeDef.Enum>().map {
it.generate(symbols)
}

// TODO generate hashCode, equals, componentN so we can have OPEN internal implementations
private fun KotlinNodeSpec.Product.addDataClassMethods(symbols: KotlinSymbols, ref: TypeRef.Path) {
impl.addModifiers(KModifier.INTERNAL, KModifier.OPEN)
addEqualsMethod()
addHashCodeMethod()
addToStringMethod(symbols, ref)
val args = listOf("_id") + props.map { it.name }
val copy = FunSpec.builder("copy").addModifiers(KModifier.ABSTRACT).returns(clazz)
val copyImpl = FunSpec.builder("copy")
.addModifiers(KModifier.OVERRIDE)
.returns(clazz)
.addStatement("return %T(${args.joinToString()})", implClazz)
props.forEach {
val para = ParameterSpec.builder(it.name, it.type).build()
copy.addParameter(para.toBuilder().defaultValue("this.${it.name}").build())
copyImpl.addParameter(para)
}
builder.addFunction(copy.build())
impl.addFunction(copyImpl.build())
}

/**
* Adds `equals` method to the core abstract class
*/
private fun KotlinNodeSpec.Product.addEqualsMethod() {
val equalsFunctionBodyBuilder = CodeBlock.builder().let { body ->
body.addStatement("if (this === other) return true")
body.addStatement("if (other !is %T) return false", this.clazz)
this.props.forEach { prop ->
body.addStatement("if (%N != other.%N) return false", prop.name, prop.name)
}
body.addStatement("return true")
}
builder.addFunction(
FunSpec.builder("equals").addModifiers(KModifier.OVERRIDE).returns(Boolean::class)
.addParameter(ParameterSpec.builder("other", Any::class.asTypeName().copy(nullable = true)).build())
.addCode(equalsFunctionBodyBuilder.build())
.build()
)
}

/**
* Adds `hashCode` method to the core abstract class
*/
private fun KotlinNodeSpec.Product.addHashCodeMethod() {
val hashcodeBodyBuilder = CodeBlock.builder().let { body ->
when (this.props.size) {
0 -> body.addStatement("return 0")
1 -> body.addStatement("return %N.hashCode()", this.props.first().name)
else -> {
body.addStatement("var result = %N.hashCode()", this.props.first().name)
this.props.subList(1, this.props.size).forEach { prop ->
body.addStatement("result = 31 * result + %N.hashCode()", prop.name)
}
body.addStatement("return result")
}
}
body
}
builder.addFunction(
FunSpec.builder("hashCode")
.addModifiers(KModifier.OVERRIDE)
.returns(Int::class)
.addCode(hashcodeBodyBuilder.build())
.build()
)
}

private fun enumToStringSpec(base: String): FunSpec {
val bodyBuilder = CodeBlock.builder().let { body ->
val str = "$base::\${super.toString()}"
body.addStatement("return %P", str)
body
}
return FunSpec.builder("toString")
.addModifiers(KModifier.OVERRIDE)
.returns(String::class)
.addCode(bodyBuilder.build())
.build()
}

/**
* Adds `toString` method to the core abstract class. We write it in Ion syntax, however, it is NOT a contract
* and therefore subject to failure.
*
* Notably, the following don't format to Ion:
* - Maps
* - Imported Types
* - Escape Characters
*/
private fun KotlinNodeSpec.Product.addToStringMethod(symbols: KotlinSymbols, ref: TypeRef.Path) {
val annotation = symbols.pascal(ref)
val thiz = this
val bodyBuilder = CodeBlock.builder().let { body ->
val returnString = buildString {
append("$annotation::{")
thiz.props.forEach { prop ->
if (String::class.asTypeName() == prop.type) {
append("${prop.name}: \"\$${prop.name}\",")
} else {
append("${prop.name}: \$${prop.name},")
}
}
append("}")
}
body.addStatement("return %P", returnString)
body
}
builder.addFunction(
FunSpec.builder("toString")
.addModifiers(KModifier.OVERRIDE)
.returns(String::class)
.addCode(bodyBuilder.build())
.build()
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,13 @@ import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.DOUBLE
import com.squareup.kotlinpoet.FLOAT
import com.squareup.kotlinpoet.INT
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.LIST
import com.squareup.kotlinpoet.LONG
import com.squareup.kotlinpoet.MAP
import com.squareup.kotlinpoet.MUTABLE_LIST
import com.squareup.kotlinpoet.MUTABLE_MAP
import com.squareup.kotlinpoet.MUTABLE_SET
import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.SET
import com.squareup.kotlinpoet.STRING
import com.squareup.kotlinpoet.TypeName
Expand Down Expand Up @@ -54,16 +51,6 @@ class KotlinSymbols private constructor(
*/
val base: ClassName = ClassName(rootPackage, "${rootId}Node")

/**
* Id Property for interfaces and classes
*/
val idProp = PropertySpec.builder("_id", String::class).addModifiers(KModifier.OVERRIDE).initializer("_id").build()

/**
* Id Parameter for internal constructors
*/
val idPara = ParameterSpec.builder("_id", String::class).build()

/**
* Memoize converting a TypeRef.Path to a camel case identifier to be used as method/function names
*/
Expand Down
Loading
Loading