All Downloads are FREE. Search and download functionalities are using the official Maven repository.

it.unibo.protelis2kotlin.Protelis2Kotlin.kt Maven / Gradle / Ivy

The newest version!
/*
 * This Kotlin source file was generated by the Gradle 'init' task.
 */
package it.unibo.protelis2kotlin
import org.gradle.api.Project
import org.gradle.api.logging.LogLevel
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.File
import java.math.BigInteger
import java.security.MessageDigest
import kotlin.text.RegexOption.DOT_MATCHES_ALL
import kotlin.text.RegexOption.MULTILINE
import java.io.File.separator as SEP

/**
 * Protelis file extension.
 */
const val PROTELIS_FILE_EXTENSION = "pt"

/**
 * Data class containing [protelisTypes] information that should be collected during parsing.
 */
private data class Context(var protelisTypes: Set = emptySet()) {
    fun registerProtelisType(type: String) {
        this.protelisTypes += type
    }
}

/**
 * Interface for a "piece of documentation".
 */
private interface DocPiece {
    companion object {

        /**
         * matches a @param tag.
         */
        val docParamRegex = """@param\s+(\w+)\s*([^\n]*)""".toRegex()

        /**
         * matches a @return tag.
         */
        val docReturnRegex = """@return\s+([^\n]*)""".toRegex()

        /**
         * matches other directives.
         */
        val docOtherDirectiveRegex = """@(\w+)\s+([^\n]*)""".toRegex()
    }

    /**
     * Creates a new [DocPiece] that extends this [DocPiece] with some [text].
     */
    fun extendWith(text: String): DocPiece
}

/**
 * Data class for a piece of documentation [text] (like this very comment).
 */
private data class DocText(val text: String) : DocPiece {
    override fun extendWith(text: String): DocPiece {
        return DocText(this.text + text)
    }
}

/**
 * Data class for a piece of documentation describing a function parameter with its [name], [type], and [description].
 */
private data class DocParam(
    val name: String,
    val type: String,
    val description: String,
) : DocPiece {
    override fun extendWith(text: String): DocPiece {
        return DocParam(name, type, description + text)
    }
}

/**
 * Data class for a piece of documentation with a [description] a function's [returnType].
 */
private data class DocReturn(
    val returnType: String,
    val description: String,
) : DocPiece {
    override fun extendWith(text: String): DocPiece {
        return DocReturn(returnType, description + text)
    }
}

/**
 * Data class for a generic documentation [directive] `@ [description]`.
 */
private data class DocDirective(
    val directive: String,
    val description: String,
) : DocPiece {
    override fun extendWith(text: String): DocPiece {
        return DocDirective(directive, description + text)
    }
}

/**
 * Data class describing a Protelis function parameter ([name] and [type]).
 */
private data class ProtelisFunArg(val name: String, val type: String)

/**
 * Data class describing a Protelis function: [name], [parameters], [returnType],
 * visibility ([public] or not), and type parameters ([genericTypes]).
 */
private data class ProtelisFun(
    val name: String,
    val parameters: List = listOf(),
    val returnType: String = "",
    val public: Boolean = false,
    val genericTypes: Set = setOf(),
)

/**
 * Data class containing the various [documentationPieces] for a Protelis function.
 */
private data class ProtelisFunDoc(val documentationPieces: List)

/**
 * Data class pairing a Protelis [function] with its [docs].
 */
private data class ProtelisItem(val function: ProtelisFun, val docs: ProtelisFunDoc)

/**
 * Parses a type and returns both the parsed type and the remaining text.
 * @param line The text line to be parsed
 */
private fun parseTypeAndRest(line: String): Pair {
    // Works by finding the first comma which is not contained within parentheses
    var stillType = true
    var k = 0
    var parentheses = ""
    val type = line.takeWhile { c ->
        k++
        val cond = (c != ',' || stillType) && !(c == ',' && k > 0 && parentheses.isEmpty())
        if (stillType && (c == '(' || c == '[')) parentheses += c
        if (stillType && (c == ')' || c == ']')) {
            parentheses = parentheses.dropLast(1)
            if (parentheses.isEmpty()) stillType = false
        }
        cond
    }
    return Pair(type, line.substring(k).trim())
}

/**
 * Parses the documentation of a Protelis function.
 * @param doc The documentation string to be parsed
 * @return [ProtelisFunDoc]
 */
private fun parseDoc(doc: String): ProtelisFunDoc {
    var txt = ""
    val pieces: MutableList = mutableListOf()
    doc.lines().map { """\s*\*\s*""".trimMargin().toRegex().replace(it, "").trim() }.forEach { partialtxt ->
        if (!partialtxt.startsWith("@")) {
            if (pieces.isEmpty()) {
                txt += if (txt.isEmpty()) partialtxt else "\n $partialtxt"
            } else {
                val last = pieces.last()
                pieces.remove(last)
                pieces.add(last.extendWith(" $partialtxt"))
            }
        } else {
            DocPiece.docParamRegex.matchEntire(partialtxt)?.let { matchRes ->
                val gs = matchRes.groupValues
                val (type, desc) = parseTypeAndRest(gs[2])
                pieces.add(DocParam(gs[1], type, desc))
                return@forEach
            }

            DocPiece.docReturnRegex.matchEntire(partialtxt)?.let { matchRes ->
                val gs = matchRes.groupValues
                val (type, desc) = parseTypeAndRest(gs[1])
                pieces.add(DocReturn(type, desc))
                return@forEach
            }

            DocPiece.docOtherDirectiveRegex.matchEntire(partialtxt)?.let { matchRes ->
                val directive = matchRes.groupValues[1]
                val desc = matchRes.groupValues[2]
                pieces.add(DocDirective(directive, desc))
                return@forEach
            }
        }
    }
    if (txt.isNotEmpty()) {
        pieces.add(0, DocText(txt))
    }
    return ProtelisFunDoc(pieces)
}

/**
 * Parses a Protelis function definition.
 * @param fline The string of a Protelis function definition to be parsed
 * @return [ProtelisFun]
 */
private fun parseProtelisFunction(fline: String): ProtelisFun {
    return ProtelisFun(
        name = checkNotNull("""def\s+(\w+)\s*\(""".toRegex().find(fline)?.groupValues?.get(1)) {
            "Cannot parse function name in: $fline"
        },
        parameters =
        """\(([^)]*)\)""".toRegex().find(fline)?.groupValues?.get(1)?.split(",")
            ?.filter { it.isNotEmpty() }
            ?.map {
                // if (!"""\w""".toRegex().matches(it)) throw IllegalStateException("Bad argument name: $it")
                ProtelisFunArg(it.trim(), "")
            }
            ?.toList()
            ?: error("Cannot parse arglist in: $fline"),
        public = "(public\\s+def)".toRegex().find(fline) != null,
    )
}

/**
 * Parses Protelis source code into a list of [ProtelisItem]s.
 * @param content The string of Protelis source code to be parsed
 */
private fun parseFile(content: String): List {
    val pitems = mutableListOf()

    """^\s*(/\*\*(.*?)\*/)?\n*((^|[\w\s]*\s)def\s[^{]*?\{)"""
        .toRegex(setOf(MULTILINE, DOT_MATCHES_ALL))
        .findAll(content)
        .forEach { matchRes ->
            val groups = matchRes.groupValues
            val doc = groups[2]
            val funLine = groups[3]
            val parsedDoc: ProtelisFunDoc = parseDoc(doc)
            // Easy check to control if we actually have a function
            if (!funLine.contains("def")) return@forEach
            val parsedFun = parseProtelisFunction(funLine)
            pitems.add(ProtelisItem(parsedFun, parsedDoc))
        }
    return pitems
}

/**
 * Generates (Dokka ) Kotlin documentation from a [ProtelisFunDoc].
 * @param docs The [ProtelisFunDoc] object encapsulating the docs for a Protelis function
 */
private fun generateKotlinDoc(docs: ProtelisFunDoc): String {
    val docPieces = docs.documentationPieces
    return "/**\n" +
        docPieces.joinToString("\n") { p ->
            when (p) {
                is DocText -> p.text.lines().joinToString("\n") { "  * $it" }
                is DocParam -> "  * @param ${p.name} ${p.description}"
                is DocReturn -> "  * @return ${p.description}"
                is DocDirective -> "  * @${p.directive} ${p.description}"
                else -> ""
            }
        } + "\n  */"
}

/**
 * Generates a Kotlin type from a Protelis type.
 */
private fun generateKotlinType(context: Context, protelisType: String): String = when (protelisType) {
    "" -> "Unit"
    "bool" -> "Boolean"
    "num" -> "Number"
    else ->
        """\(([^)]*)\)\s*->\s*(.*)""".toRegex().matchEntire(protelisType)?.let { matchRes ->
            val args = matchRes.groupValues[1].split(",").map { generateKotlinType(context, it.trim()) }
            val ret = generateKotlinType(context, matchRes.groupValues[2])
            """(${args.joinToString(",")}) -> $ret"""
        } ?: """\[.*]""".toRegex().matchEntire(protelisType)?.let { _ ->
            context.registerProtelisType("Tuple")
            "Tuple"
        } ?: if (protelisType.length == 1 && protelisType.any { it.isUpperCase() }) {
            protelisType
        } else if ("""[A-Z]'""".toRegex().matches(protelisType)) {
            "${protelisType[0].inc()}"
        } else if ("""\w+""".toRegex().matches(protelisType)) {
            context.registerProtelisType(protelisType)
            protelisType
        } else {
            "Any"
        }
}

/**
 * Valid Protelis symbols that are not valid in Kotlin (e.g., as they are reserved words) are sanitized.
 */
private fun sanitizeNameForKotlin(name: String): String = when (name) {
    "null" -> "`null`"
    else -> name
}

/**
 * Generates a Kotlin function from a Protelis function descriptor.
 */
private fun generateKotlinFun(context: Context, fn: ProtelisFun): String {
    var genTypesStr = fn.genericTypes.joinToString(",")
    if (genTypesStr.isNotEmpty()) genTypesStr = " <$genTypesStr>"
    return "@Suppress(\"UNUSED_PARAMETER\")\nfun$genTypesStr ${sanitizeNameForKotlin(fn.name)}(" +
        fn.parameters.joinToString(", ") {
            "${sanitizeNameForKotlin(it.name)}: ${
                generateKotlinType(
                    context,
                    it.type,
                )
            }"
        } +
        "): ${generateKotlinType(context, fn.returnType)} = TODO()"
}

/**
 * Generates a Kotlin item (doc + fun signature) from a Protelis item (doc + fun).
 */
private fun generateKotlinItem(context: Context, pitem: ProtelisItem): String {
    val doc = pitem.docs
    val fn = pitem.function
    return generateKotlinDoc(doc) + "\n" + generateKotlinFun(context, fn)
}

/**
 * Generates a string from a list of Protelis items (function and docs pairs).
 */
private fun generateKotlin(context: Context, protelisItems: List): String = protelisItems.map { item ->
    val doc = item.docs
    val fn = item.function
    item.copy(
        function = fn.copy(
            returnType = doc.documentationPieces
                .filterIsInstance()
                .map { it.returnType }
                .firstOrNull()
                .orEmpty(),
            parameters = fn.parameters
                .map { param ->
                    param.copy(
                        type = doc.documentationPieces
                            .filter { it is DocParam && it.name == param.name }
                            .map { (it as DocParam).type }
                            .firstOrNull()
                            ?: "Any",
                    )
                },
            genericTypes = doc.documentationPieces
                .map { if (it !is DocParam) "" else it.type }
                .flatMap { type ->
                    "([A-Z]'?)".toRegex()
                        .findAll(type)
                        .map {
                            if (it.value.length == 2 && it.value[1] == '\'') {
                                "${it.value[0].inc()}"
                            } else {
                                it.value
                            }
                        }
                        .toList()
                }.toSet(),
        ),
    )
}.joinToString("\n\n") { generateKotlinItem(context, it) }

/**
 * Turns a Protelis package to a class name using camelcase convention.
 */
private fun packageToClassName(pkg: String): String {
    return pkg.split(':').last().split('_')
        .joinToString("") { it.replaceFirstChar(Char::titlecaseChar) }
}

@Suppress("MagicNumber")
private fun String.sha256(): String =
    BigInteger(MessageDigest.getInstance("SHA-256").digest(toByteArray())).toString(36)

/**
 * Main function: calls [protelis2Kt] without a project.
 *
 * This is to be called with two arguments:
 * 1) The base directory from which recursively looking for Protelis files
 * 2) The destination directory that will contain the output Kotlin files
 */
fun main(args: Array) {
    require(args.size == 2) {
        "USAGE: program   "
    }
    val noProject: Project? = null
    noProject.protelis2Kt(args[0], args[1])
}

/**
 * Reads all Protelis files under a [base] directory, parses them,
 * and generates corresponding Kotlin files in a [destination] directory.
 *
 * This is to be called with two arguments:
 * 1) The base directory from which recursively looking for Protelis files
 * 2) The destination directory that will contain the output Kotlin files
 */
fun Project?.protelis2Kt(base: String, destination: String) {
    val header = "Protelis2Kt"
    val logger = this?.logger ?: object : org.gradle.api.logging.Logger, Logger by LoggerFactory.getLogger(header) {
        override fun isEnabled(level: LogLevel?) = true
        override fun isLifecycleEnabled() = true
        override fun isQuietEnabled(): Boolean = true
        override fun lifecycle(message: String?) = debug(message)
        override fun lifecycle(message: String?, vararg objects: Any?) = debug(message, objects)
        override fun lifecycle(message: String?, throwable: Throwable?) = warn(message, throwable)
        override fun quiet(message: String?) = info(message)
        override fun quiet(message: String?, vararg objects: Any?) = info(message, objects)
        override fun quiet(message: String?, throwable: Throwable?) = warn(message, throwable)
        override fun log(level: LogLevel?, message: String?) = log(level, message, *emptyArray())
        override fun log(level: LogLevel?, message: String?, vararg objects: Any?) = lifecycle(message, objects)
        override fun log(level: LogLevel?, message: String?, throwable: Throwable?) = warn(message, throwable)
    }
    logger.debug("$header base directory: $base\n$header destination directory: $destination")
    var k = 0
    val root = this?.file(base) ?: File(base)
    logger.debug("fetching Protelis files in {}", root.absolutePath)
    root.walkTopDown()
        .filter { it.isFile && it.extension == PROTELIS_FILE_EXTENSION }
        .forEach { file ->
            val fileText: String = file.readText()
            logger.debug("Processing " + file.absolutePath)
            val pkg = "module (.+)".toRegex().find(fileText)?.groupValues?.component2()
                ?: "anonymous_module_${fileText.sha256()}"
            val pkgParts = pkg.split(':')
            val context = Context()
            val protelisItems: List = parseFile(fileText)
            val pkgCode =
                """
                @file:JvmName("${packageToClassName(pkg)}")
                package ${pkgParts.joinToString(".")}
                """.trimIndent()
            val kotlinCode = generateKotlin(context, protelisItems)
            val importCode = context.protelisTypes
                .map {
                    when (it) {
                        "ExecutionContext", "ExecutionEnvironment" -> "org.protelis.vm.$it"
                        "Tuple" -> "org.protelis.lang.datatype.$it"
                        else -> ""
                    }
                }
                .filterNot { it.isEmpty() }
                .joinToString("\n") { "import $it" } + "\n\n"
            val kotlinFullCode = pkgCode + importCode + kotlinCode
            val outPath = "$destination$SEP${pkgParts.joinToString(SEP)}$SEP${file.name.replace(".pt",".kt")}"
            File(outPath).let {
                it.parentFile.mkdirs()
                it.createNewFile()
                it
            }.writeText(kotlinFullCode)
            k++
        }
    logger.lifecycle("$header Converted $k .pt files to Kotlin")
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy