Best way to implement "independent" DSL elements

I try to describe Kotlin syntax itself with a DSL, and I struggle a bit with the more “loose” or “independent” elements, like annotations or modifiers. Take this piece of code:

private const val foo: String = "bar"

The best I could come up with my DSL is

PRIVATE(); CONST(); VAL("foo", String::class, "bar")

Behind the scenes it works by adding PRIVATE and CONST to a mutable list in the scope, and then the VAL function collects its modifiers from this list and clears it. I will implement a similar solution for annotations.

In the example I’m less concerned with the VAL part, I can certainly improve it, but I don’t like the modifiers, e.g. the need for semicolons. Something like +PRIVATE; +CONST; ... would save a character, but I’m not sure if I like it.

I would love something like @PRIVATE @CONST ..., but there seems no way to get this syntax to work, at least not without annotation processors or compiler plugins.

Did I miss a trick, or is there really no more wiggle room regarding the syntax?

Hmm, what if you don’t try to replicate the exact code flow/ordering and instead do:

VAL("foo", String::class, "bar", PRIVATE, CONST)

It makes sense as private and const are modifiers to val.

But I know, this is not a direct answer to your question.

This is still a valid question. I think there are many more things to consider (like getters and setters), so VAL may end up pretty crowded. Therefore, getting modifiers (and annotations) out of the way seems to be the better approach. In my opinion, having to remember where to put the modifiers in a long argument list feels a bit like defeating the purpose of a DSL.

And of course, this is just an example. The modifier problem occurs basically everywhere, and I’m not sure if I should “waste” the only available (*) vararg parameter for this.

*) I know I can have multiple varargs in an argument list, but then I cannot use them without either named access or spread operator, so in the context of a DSL there is just one “good” vararg.

Hmmm, if you make your annotations have BINARY retention, then I think you could maybe do something with delegated properties?

@PRIVATE
@CONST
val foo: String by magicDslFunction("bar")

where magicDslFunction is reified, and returns something with a provideDelegate that reads the name and annotations of the property (I think you can use reflection to access the annotations on a local delegated property but I’m not 100% certain)

Interesting idea, but I think I need VAL to be a function in order to allow for a dynamic code creation.

1 Like

magicDslFunction and provideDelegate are your friends here. They’ll let you do exactly any dynamic code creation that you want. Think of the provided example as translating to:

val kprop = SomeIntrinsicKPropertyConstructor(name = "foo", annotations = listOf(PRIVATE, CONST), type = String::class)
magicDslFunction<String>("bar").provideDelegate(null, kprop)

and so inside of provideDelegate you can access all the necessary info.
If you have a repo with your WIP code, I could make a little example of how to implement this, if you’d like!
This syntax can also be tweaked a bit so that you can refer to foo for instance later on in the generated code, and get type checking for free!

Something like this should work (playground doesn’t have jvm reflection, so the annotation reading doesn’t work. Should work fine as long as you add that dependency though):

import kotlin.reflect.*
fun main() {
    println(build {
        @PRIVATE
        @CONST
        val foo by VAL("bar")
    })
}

annotation class PRIVATE
annotation class CONST
enum class Visibility(val display: String) {
    Private("private"),
    Internal("internal"),
    ;
    override fun toString() = display
}

sealed interface Expression<T>
operator fun <E: Expression<T>, T> E.getValue(thisRef: Any?, prop: KProperty<*>): E = this
class Const<T>(val value: T): Expression<T> {
    override fun toString() = if(value is String) """"$value"""" else "$value"
}
class Local<T>(val name: String): Expression<T> {
    override fun toString() = name
}

class KotlinBuilder {
    internal val output = StringBuilder()
	fun <T> VAL(initial: T): LocalDelegate<T> = VAL(Const(initial))
    fun <T> VAL(initial: Expression<T>): LocalDelegate<T> = LocalDelegate(initial)
    fun <T> add(local: Local<T>, type: KType, value: Expression<T>, visibility: Visibility = Visibility.Internal, isConst: Boolean = false) {
        output.append("$visibility ${if(isConst) "const " else ""}val ${local.name}: ${(type.classifier!! as KClass<*>).simpleName} = $value")
    }
    
	inline operator fun <reified T> LocalDelegate<T>.provideDelegate(thisRef: Any?, prop: KProperty<*>): Local<T> = Local<T>(prop.name).also{
        val visibility = if(prop.annotations.any { it is PRIVATE }) Visibility.Private else Visibility.Internal
        val isConst = prop.annotations.any { it is CONST }
        add(it, typeOf<T>(), expr, visibility, isConst)
    }
}

class LocalDelegate<T>(val expr: Expression<T>)

fun build(block: KotlinBuilder.() -> Unit) = KotlinBuilder().apply(block).output.toString()

I made a lot of assumptions and simplifications in designing this, but the basic idea should be easy to see

I think there is a misunderstanding regarding what I mean with “dynamic”. E.g. I want to be able to create vals in a loop for my DSL:

val file = FILE("funny.kt", "one.two.three") {
        for (n in 1 .. 10) { 
            PRIVATE(); VAL("foo$n", String::class)
        }
    }

And I think “real” vals wouldn’t work here.

I might be wrong though, so if you want to try, here is a repo: GitHub - DanielGronau/koloom: Kotlin code generator

How about in addition to the objects for each modifier defined the infix function, which takes KModifier as its arguments and returns the KModifier, which represents the combination of its arguments.
This way you can chain modifiers without parentheses and semicolons.

I think I’ll go with an operator. It’s not pretty, especially because .. evaluates from left to right. Here is a stripped down example, in case anyone needs something similar:

enum class Mod {
    PUBLIC,
    PRIVATE,
    ABSTRACT
}

data class Value(val name: String, val mods: MutableList<Mod> = mutableListOf<Mod>())

object Terminal

class Scope {
    val values = mutableListOf<Value>()

    fun VAL(name: String) = Terminal.also {
        values += Value(name)
    }

    operator fun Mod.rangeTo(mc: Terminal) {
        values.last().mods += this
    }

    operator fun List<Mod>.rangeTo(mc: Terminal) {
        values.last().mods += this
    }

    operator fun Mod.rangeTo(mod: Mod) = listOf(this, mod)

    operator fun List<Mod>.rangeTo(mod: Mod) = this + mod
}

fun scope(lambda: Scope.() -> Unit): List<Value> =
    Scope().apply(lambda).values

fun main() {
    val list = scope {
        VAL("a")
        PRIVATE .. VAL("b")
        PRIVATE .. PUBLIC .. VAL("c")
        ABSTRACT .. PRIVATE .. PUBLIC .. VAL("d")
    }
    println(list)
}