Skip to content

Asta.kt

src/jvmMain/kotlin/afield/lang/remotejs/Asta.kt · 424 lines

package afield.lang.remotejs

import afield.app.notifications.notify
import org.openjdk.nashorn.api.tree.*
import java.lang.reflect.Method
import java.util.*
import java.util.function.Function
import kotlin.Pair
import kotlin.to

Source-to-source rewriting of a JavaScript box, over Nashorn's parse tree.

JavaScript has no operator overloading, so in a RemoteJS box a + b on two vectors concatenates their string forms and a - b is NaN. This pass rewrites the source before it ships: each binary +, -, *, / and ^ becomes a call to a global named for the operator, so a + b leaves here as __PLUS__(a,b). The head supplies those globals in field/asta.js, where __PLUS__, __MINUS__, __MULTIPLY__ and __DIVIDE__ work componentwise on THREE vectors, promote an array of two or three numbers to a vector first, let an operand join the protocol by defining its own __PLUS__ (or __RPLUS__, for the right-hand side), and throw a named error where plain JavaScript would hand back NaN or "[object Object]". XOR is in the trap set here and has no counterpart there.

executeRemoteJS runs the pass when its doASTA flag is set, which is the default. The Kotlin/JS path sets it false, because Kotlin has operator fun plus of its own and a compiled box needs no rewriting.

The pass also recovers a source location. Nashorn parses at ES6 while the browser is the authority on whether newer syntax is valid, so a diagnostic raised here is held rather than shown, and used only to fill in the line and column of a SyntaxError that the head reported without one; an error the head raises at run time keeps its own location. A parser AssertionError is caught, notified, and the source returned unrewritten, so a construct Nashorn cannot read costs the operators rather than the run.

Rewriting is textual, so offsets do the work, and Nashorn's need correcting before they can index the string. A string literal's quotes fall outside its reported extent, and a node that reports a narrower span than its own children is widened to cover them. Replacements run back-to-front so an earlier edit cannot move a later one, and the header of a for loop is excluded from rewriting entirely, because an edit inside it shifts the offsets of everything the loop spans. Field's //# sourceURL= trailer is split off before parsing and appended again afterwards.

Options gates the whole pass. overloads is on, and is the operator rewrite above; numbers is off, and rewrites each numeric literal to __NUMBER_LITERAL__(start, end, text), for which this tree carries no head-side definition; functionRewrite is off and has no effect beyond making the pass run at all. With all three off, parseAndReconstruct returns its input unchanged.

class Asta

One rewrite pass over one fragment: set options, then call parseAndReconstruct.

executeRemoteJS constructs a fresh instance per fragment, and accessorMap is reflected over every Tree.Kind once per instance. Of the mutable state here only options is read: disabled, currentMapping, lastReturn, currentContents and baseContents are declared and never touched again, and the hash threaded down the recursion reaches no decision.

class Asta {

val disabled = false

class Options

var overloads = true

var numbers = false

var functionRewrite = false

fun on(): Boolean = overloads || numbers || functionRewrite

var options = Options()

var currentMapping = mutableMapOf<Pair<Int, Int>, Pair<Int, Int>>()

var lastReturn = mutableMapOf<Pair<Int, Int>, Double>()

var currentContents: String? = null

var baseContents: String? = null

val accessorMap: Map<Tree.Kind, List<Method>>

    val accessorMap: Map<Tree.Kind, List<Method>> = Tree.Kind.values().filter { it.asInterface() != null }.map {
        val access = it.asInterface().declaredMethods.filter {
            (it.parameterCount == 0 && Tree::class.java.isAssignableFrom(it.returnType)) || (it.parameterCount == 0 && List::class.java.isAssignableFrom(
                it.returnType
            ))
        }.toMutableList()

        if (it == Tree.Kind.FOR_IN_LOOP) {
            // also do ForOfLoop
            access.addAll(ForOfLoopTree::class.java.declaredMethods.filter {
                (it.parameterCount == 0 && Tree::class.java.isAssignableFrom(it.returnType)) || (it.parameterCount == 0 && List::class.java.isAssignableFrom(
                    it.returnType
                ))
            }.toList())
        }

        it to access
    }.toMap()

val trapKinds

    val trapKinds = setOf(Tree.Kind.PLUS, Tree.Kind.MINUS, Tree.Kind.DIVIDE, Tree.Kind.MULTIPLY, Tree.Kind.XOR)

fun parse(v: String)

    fun parse(v: String) {
        val p = Parser.create("--language=es6")
        val r = p.parse("<<internal>>", v, {
            print("diagnostic: $it")
        })

        r.sourceElements.forEach {
            //            print("\n\n sourceElement: $it\n\n")
            recurOver(it, 0, v)
        }

    }

var hash = 0L

fun parseAndReconstruct(v: String, d: (Diagnostic) -> Unit

body — 291 lines, from line 113
    @JvmOverloads
    fun parseAndReconstruct(v: String, d: (Diagnostic) -> Unit = { print("diagnostic: $it") }): String {

        if (!options.on()) return v

        try {
            val (trimmed, suffix) = trimSrcURL(v)


            val p = Parser.create("--language=es6")
            val r = p.parse("<<internal>>", trimmed + "\n", d)

            var actions = mutableListOf<() -> Unit>()

            var replaced = trimmed

            var forbiddenRanges = mutableListOf<Pair<Long, Long>>();

            r.sourceElements.forEach {
                findForbiddenRanges(it, forbiddenRanges);
            }


            hash = 0L

            r.sourceElements.forEach {
                //            print("\n\n sourceElement: $it\n\n")

                var (s, e) = startAndEndPositionFor(it)

                actions.add {
                    replaced = replaced.replaceRange(
                        s,
                        e,
                        reconstructOver(it, 0, v, forbiddenRanges, parent = null, hash = 0L)
                    )
                }
            }

            actions.reversed().forEach { it() }

            return replaced + "\n" + suffix
        }
        catch(a : AssertionError) {
            notify("ASTA parser crashed in code, returning code as-is", "error")
            return v
        }
    }

    private fun findForbiddenRanges(it: Tree, forbiddenRanges: MutableList<Pair<Long, Long>>) {

        if (it is ForLoopTree) {
            val a = it.startPosition
            val b = it.statement.startPosition
            forbiddenRanges.add(a to b)
        }

        childrenFor(it).forEach { findForbiddenRanges(it, forbiddenRanges) }

    }

    private fun recurOver(tree: Tree, i: Int, v: String) {
        val children = childrenFor(tree)

        var (start, end) = startAndEndPositionFor(tree)

        var text = v.substring(start, end)

//        if (debug) println(" node covers ${start} -> ${end} = $text")

        var n = children.size - 1
        for (c in children.asReversed()) {

//            if (debug) println(" replace range ${(c.startPosition - start).toInt()..(c.endPosition - start - 1).toInt() - 1}  : '" + text + "'")

            val (s, e) = startAndEndPositionFor(c)

            text = text.replaceRange((c.startPosition - start).toInt()..(c.endPosition - start - 1).toInt(), "<<$n>>")
            n--
        }


        children.forEach { recurOver(it, i + 2, v) }
    }

    private fun reconstructOver(
        tree: Tree,
        i: Int,
        v: String,
        forbiddenRanges: List<Pair<Long, Long>>,
        parent: Tree? = null,
        hash: Long
    ): String {

        val children = childrenFor(tree)

        var (start, end) = startAndEndPositionFor(tree, true)

        var text = v.substring(start, end)


//        if (debug) println("${indent(i)} node covers ${start} -> ${end} = $text")

        if (options.overloads && tree is BinaryTree && tree.kind in trapKinds && !forbidden(
                tree.startPosition,
                tree.endPosition,
                forbiddenRanges
            )
        ) {
//            if (debug) println(" ** binary operator :" + tree)

//            if (!(text.contains("'") || text.contains("\"") || text.contains("`")))
            if (true) {

                // is this inside the initializer of a for loop? if so, replacing anything here will wreck everyone's offsets

                val left = children.get(0)
                val right = children.get(1)

                val (sr, er) = startAndEndPositionFor(right)
                var rightR = reconstructOver(
                    right,
                    i + 5,
                    v,
                    forbiddenRanges,
                    parent,
                    hash = 31 * hash + hashFor(right, "right".hashCode().toLong())
                )//text.replaceRange((sr - start).toInt()..(er - start - 1).toInt(), )


                val (sc, ec) = startAndEndPositionFor(left)
                var leftR = reconstructOver(
                    left,
                    i + 5,
                    v,
                    forbiddenRanges,
                    parent,
                    hash = 31 * hash + hashFor(left, "left".hashCode().toLong())
                )//rightR.replaceRange((sc - start).toInt()..(ec - start - 1).toInt())


                val middle = v.substring(ec, sr)

                // search for missing brackets --- Some parens are part of no element apparently

                var closeBracket = count(middle, ')');
                val openBracket = count(middle, '(');

//                println(" middle text is `$middle` $closeBracket, $openBracket")
//                println("left `$leftR`")
//                println("right `$rightR`")

                if (rightR.trim().endsWith("//"))
                    rightR = rightR.trim().substring(0, rightR.trim().length - 2)

                if (leftR.startsWith("(") && count(leftR, '(') == count(leftR, ')') + 1 && closeBracket > 0) {
                    leftR = leftR.substring(1)
                    closeBracket--
                }
                if (leftR.endsWith(")") && count(leftR, ')') == count(
                        leftR,
                        '('
                    ) + 1 && closeBracket == 0 && openBracket == 0
                ) {
                    leftR = leftR.substring(0, leftR.length - 1)
                    closeBracket++
                }

                return duplicate(openBracket, "(") + "__" + tree.kind + "__(" + leftR + "," + rightR + ")" + duplicate(
                    closeBracket,
                    ")"
                )
            }
        }

        if (options.numbers && tree is LiteralTree && tree.kind.equals(Tree.Kind.NUMBER_LITERAL)) {
            return "__NUMBER_LITERAL__(" + start + ", " + end + ", " + text + ")"
        }


        var n = children.size - 1
        for (c in children.asReversed()) {

            val (s, e) = startAndEndPositionFor(c)

            text = text.replaceRange(
                (s - start)..(e - start - 1),
                reconstructOver(
                    c,
                    i + 5,
                    v,
                    forbiddenRanges,
                    parent = tree,
                    hash = hash * 31 + hashFor(c, n.toLong())
                )
            )
            n--
        }


        return text
    }

    private fun hashFor(c: Tree, salt: Long = 0): Long {
        return c.kind.hashCode().toLong() * 31 + salt
    }


    private fun forbidden(startPosition: Long, endPosition: Long, f: List<Pair<Long, Long>>): Boolean {
        f.forEach {
            if (startPosition < it.second && endPosition > it.first) return true
        }
        return false
    }

    private fun count(middle: String, s: Char): Int {
        var x = 0;
        for (a in middle) {
            if (a == s) x++
        }
        return x;
    }

    private fun duplicate(num: Int, s: String): String {
        var r = ""
        for (i in 0 until num)
            r += s
        return r
    }

    private fun childrenFor(tree: Tree): List<Tree> {
//        if (debug) println(" children for :" + tree + " " + tree.javaClass + " " + tree.kind)
        return accessorMap[tree.kind]!!.flatMap {
            //            if (debug) println("-> $it ($tree)")
            try {
                try {
                    val rr = it.invoke(tree)
                    when (rr) {
                        null -> {
                            Collections.EMPTY_LIST
                        }

                        is Tree -> Collections.singleton(rr)
                        is List<*> -> rr
                        else -> {
                            Collections.EMPTY_LIST
                        }
                    }
                } catch (ee: ClassCastException) {
                    // this happens when we call forloop stuff on a forofloop
//                    ee.printStackTrace()
                    Collections.EMPTY_LIST
                }
            } catch (e: java.lang.IllegalArgumentException) {
//                e.printStackTrace()
                Collections.EMPTY_LIST
            }
        }.filterIsInstance<Tree>().sortedBy { it.startPosition }.toList()
    }

    private fun startAndEndPositionFor(c: Tree, correct: Boolean = true): Pair<Int, Int> {
        var start = c.startPosition.toInt()
        var end = c.endPosition.toInt()

//        println("start/end in $c = $start/$end")
        // Nashorn appears to just lie about the start and end position of some parts of the tree.

        if (c.kind == Tree.Kind.STRING_LITERAL) {
            return start - 1 to end + 1
        }

        if (/*start == end &&*/ correct) {
            val cc = childrenFor(c)
            if (cc.size == 0) return start to end

            val ccSE = cc.map { startAndEndPositionFor(it) }.toList()
            val min = ccSE.minByOrNull { it: Pair<Int, Int> -> it.first }!!.first
            val max = ccSE.maxByOrNull { it: Pair<Int, Int> -> it.second }!!.second
            val r = Math.min(min, start) to Math.max(max, end)

//            println("start/end out $c = $r")
            return r;
        }
        return start to end
    }

    private fun indent(i: Int): String {
        var q = ""
        while (q.length < i) q = q + " "
        return q
    }

fun options(): Options

    fun options(): Options {
        return options
    }

Field source urls look like: //# sourceURL=bxUntitled/_d5b9fda0_5d2b_40a3_96c8_7926a9fb56cb

var srcURL = Regex("(\\/\\/# sourceURL=bx\\[.*\\]\\/_........_...._...._...._............)")

sourceURL=bxviewport/viewport/_f46e4d28_e0e1_453b_b698_ae4352804e51

fun trimSrcURL(src: String): Pair<String, String>

    fun trimSrcURL(src: String): Pair<String, String> {
        val res = srcURL.find(src)
        if (res != null) {
            return srcURL.replace(src, "") to res.groups[0]!!.value
        }
        return src to ""
    }

}