Skip to content

LabelledArguments.kt

src/jvmMain/kotlin/afield/runtime/macros/LabelledArguments.kt · 93 lines

package afield.runtime.macros

import afield.runtime.kotlin.FieldKtParser
import afield.runtime.lifecycle.RunsAt
import afield.runtime.lifecycle.macroRegistration
import afield.runtime.macros.MacroRegistry.callRewrite
import org.jetbrains.kotlin.psi.KtBinaryExpression
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtStringTemplateExpression
import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType

A macro that labels a function's positional arguments with their own source text, so showMeshGrid(cube, ring) compiles as showMeshGrid("cube" to cube, "ring" to ring).

Annotate a function with LabelsArgumentsFromSource to opt it in; LabelledArgumentsTest covers the rewrite.

annotation class LabelsArgumentsFromSource

Labels a function's positional arguments with their own source text, before the box compiles.

A function whose last parameter is vararg panels: Pair<String, T> wants a label for every value, and the label is nearly always the name the box already gave it. Annotated with this, the call

showMeshGrid(cube, ball.subdivided(2), "the torus" to ring, columns = 3)

is rewritten to

showMeshGrid("cube" to cube, "ball.subdivided(2)" to ball.subdivided(2), "the torus" to ring, columns = 3)

Arguments that already carry a label ("..." to x), string literals, named arguments and spreads are left alone, so an explicit label always wins. The rewrite is by name, like every Field macro: it applies to any call of a function of that name in box code.

Found by the same classpath scan that finds RunsAt functions, so annotating a function is all it takes; there is no list to keep in step.

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class LabelsArgumentsFromSource

fun registerLabelledArgumentMacros()

Registers a labelArgumentsFromSource rewrite for every function annotated LabelsArgumentsFromSource.

@RunsAt(macroRegistration)
fun registerLabelledArgumentMacros() {
    afield.runtime.lifecycle.labelledArgumentFunctions.forEach { name ->
        callRewrite[name] = { _, id, args -> labelArgumentsFromSource(id, args) }
    }
}

fun labelArgumentsFromSource(callee: String, arguments: List<String>): Insert?

The call callee``(``arguments``) with each unlabelled positional argument written as "<its source>" to <it>, or null when every argument is already labelled (which is also what ends the expander's re-run loop on the rewritten call).

@param arguments the source text after the callee, as the expander hands it over: the parenthesised argument list first, then any trailing lambda

fun labelArgumentsFromSource(callee: String, arguments: List<String>): Insert? {
    val listIndex = arguments.indexOfFirst { it.trimStart().startsWith("(") }
    if (listIndex < 0) return null
    val call = try {
        // The outermost call: descendants come innermost first, and an argument may be a call itself.
        FieldKtParser.parse("$callee${arguments[listIndex]}").collectDescendantsOfType<KtCallExpression>()
            .filter { it.calleeExpression?.text == callee }.minByOrNull { it.textRange.startOffset }
    } catch (t: Throwable) {
        null
    } ?: return null

    var changed = false
    val rewritten = call.valueArguments.map { argument ->
        val expression = argument.getArgumentExpression()
        val labelled = argument.isNamed() || argument.getSpreadElement() != null || expression == null ||
            expression is KtStringTemplateExpression ||
            (expression is KtBinaryExpression && expression.operationReference.text == "to")
        if (labelled) argument.text
        else {
            changed = true
            "${kotlinStringLiteral(expression.text.replace(Regex("""\s+"""), " "))} to ${expression.text}"
        }
    }
    if (!changed) return null
    val rest = arguments.drop(listIndex + 1).joinToString("")
    return Insert(here = "$callee(${rewritten.joinToString(", ")})$rest")
}

private fun kotlinStringLiteral(text: String): String =
    "\"" + text.replace("\\", "\\\\").replace("\"", "\\\"").replace("$", "\\$") + "\""