blob: 91f30372c500f432dc75886c02f80998f430c4e5 [file] [log] [blame]
/*
* Copyright 2017-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
package kotlinx.atomicfu.transformer
import org.mozilla.javascript.*
import org.mozilla.javascript.ast.*
import java.io.File
import java.io.FileReader
import org.mozilla.javascript.Token
import java.util.regex.*
private const val ATOMIC_CONSTRUCTOR = """(atomic\$(ref|int|long|boolean)\$|Atomic(Ref|Int|Long|Boolean))"""
private const val ATOMIC_CONSTRUCTOR_BINARY_COMPATIBILITY = """(atomic\$(ref|int|long|boolean)\$1)""" // mangled names for declarations left for binary compatibility
private const val ATOMIC_ARRAY_CONSTRUCTOR = """(atomicfu)\$(Atomic(Ref|Int|Long|Boolean)Array)\$(ref|int|long|boolean|ofNulls)"""
private const val MANGLED_VALUE_PROP = "kotlinx\$atomicfu\$value"
private const val TRACE_CONSTRUCTOR = "atomicfu\\\$Trace"
private const val TRACE_BASE_CLASS = "atomicfu\\\$TraceBase"
private const val TRACE_APPEND = """(atomicfu)\$(Trace)\$(append)\$([1234])""" // [1234] is the number of arguments in the append overload
private const val TRACE_NAMED = "atomicfu\\\$Trace\\\$named"
private const val TRACE_FORMAT = "TraceFormat"
private const val TRACE_FORMAT_CONSTRUCTOR = "atomicfu\\\$$TRACE_FORMAT"
private const val TRACE_FORMAT_FORMAT = "atomicfu\\\$$TRACE_FORMAT\\\$format"
private const val RECEIVER = """(\$(receiver)(_\d+)?)"""
private const val SCOPE = "scope"
private const val FACTORY = "factory"
private const val REQUIRE = "require"
private const val PROTOTYPE = "prototype"
private const val KOTLINX_ATOMICFU = "'kotlinx-atomicfu'"
private const val KOTLINX_ATOMICFU_PACKAGE = "kotlinx.atomicfu"
private const val KOTLIN_TYPE_CHECK = "Kotlin.isType"
private const val ATOMIC_REF = "AtomicRef"
private const val MODULE_KOTLINX_ATOMICFU = "\\\$module\\\$kotlinx_atomicfu"
private const val ARRAY = "Array"
private const val FILL = "fill"
private const val GET_ELEMENT = "atomicfu\\\$get"
private const val ARRAY_SIZE = "atomicfu\$size"
private const val LENGTH = "length"
private const val LOCKS = "locks"
private const val REENTRANT_LOCK_ATOMICFU_SINGLETON = "$LOCKS.atomicfu\\\$reentrantLock"
private val MANGLE_VALUE_REGEX = Regex(".${Pattern.quote(MANGLED_VALUE_PROP)}")
// matches index until the first occurence of ')', parenthesised index expressions not supported
private val ARRAY_GET_ELEMENT_REGEX = Regex(".$GET_ELEMENT\\((.*)\\)")
class AtomicFUTransformerJS(
inputDir: File,
outputDir: File
) : AtomicFUTransformerBase(inputDir, outputDir) {
private val atomicConstructors = mutableSetOf<String>()
private val fieldDelegates = mutableMapOf<String, String>()
private val delegatedProperties = mutableMapOf<String, String>()
private val atomicArrayConstructors = mutableMapOf<String, String?>()
private val traceConstructors = mutableSetOf<String>()
private val traceFormatObjects = mutableSetOf<String>()
override fun transform() {
info("Transforming to $outputDir")
inputDir.walk().filter { it.isFile }.forEach { file ->
val outBytes = if (file.isJsFile()) {
println("Transforming file: ${file.canonicalPath}")
transformFile(file)
} else {
file.readBytes()
}
file.toOutputFile().mkdirsAndWrite(outBytes)
}
}
private fun File.isJsFile() =
name.endsWith(".js") && !name.endsWith(".meta.js")
private fun transformFile(file: File): ByteArray {
val p = Parser(CompilerEnvirons())
val root = p.parse(FileReader(file), null, 0)
root.visit(DependencyEraser())
root.visit(AtomicConstructorDetector())
root.visit(FieldDelegatesVisitor())
root.visit(DelegatedPropertyAccessorsVisitor())
root.visit(TransformVisitor())
root.visit(AtomicOperationsInliner())
return root.eraseGetValue().toByteArray()
}
// erase getting value of atomic field
private fun AstNode.eraseGetValue(): String {
var res = this.toSource()
val primitiveGetValue = MANGLE_VALUE_REGEX
val arrayGetElement = ARRAY_GET_ELEMENT_REGEX
while (res.contains(arrayGetElement)) {
res = res.replace(arrayGetElement) { matchResult ->
val greedyToLastClosingParen = matchResult.groupValues[1]
var balance = 1
var indexEndPos = 0
for (i in 0 until greedyToLastClosingParen.length) {
val c = greedyToLastClosingParen[i]
if (c == '(') balance++
if (c == ')') balance--
if (balance == 0) {
indexEndPos = i
break
}
}
val closingParen = indexEndPos == greedyToLastClosingParen.lastIndex
if (balance == 1) {
"[$greedyToLastClosingParen]"
} else {
"[${greedyToLastClosingParen.substring(0, indexEndPos)}]${greedyToLastClosingParen.substring(indexEndPos + 1)}${if (!closingParen) ")" else ""}"
}
}
}
return res.replace(primitiveGetValue) { "" }
}
inner class DependencyEraser : NodeVisitor {
private fun isAtomicfuDependency(node: AstNode) =
(node.type == Token.STRING && node.toSource() == KOTLINX_ATOMICFU)
private fun isAtomicfuModule(node: AstNode) =
(node.type == Token.NAME && node.toSource().matches(Regex(MODULE_KOTLINX_ATOMICFU)))
override fun visit(node: AstNode): Boolean {
when (node.type) {
Token.ARRAYLIT -> {
// erasing 'kotlinx-atomicfu' from the list of defined dependencies
val elements = (node as ArrayLiteral).elements as MutableList
val it = elements.listIterator()
while (it.hasNext()) {
val arg = it.next()
if (isAtomicfuDependency(arg)) {
it.remove()
}
}
}
Token.FUNCTION -> {
if (node is FunctionNode) {
val it = node.params.listIterator()
while (it.hasNext()) {
// erasing 'kotlinx-atomicfu' module passed as parameter
if (isAtomicfuModule(it.next())) {
it.remove()
}
}
}
}
Token.CALL -> {
if (node is FunctionCall && node.target.toSource() == FACTORY) {
val it = node.arguments.listIterator()
while (it.hasNext()) {
val arg = it.next()
when (arg.type) {
Token.GETELEM -> {
// erasing 'kotlinx-atomicfu' dependency as factory argument
if (isAtomicfuDependency((arg as ElementGet).element)) {
it.remove()
}
}
Token.CALL -> {
// erasing require of 'kotlinx-atomicfu' dependency
if ((arg as FunctionCall).target.toSource() == REQUIRE) {
if (isAtomicfuDependency(arg.arguments[0])) {
it.remove()
}
}
}
}
}
}
}
Token.GETELEM -> {
if (isAtomicfuDependency((node as ElementGet).element)) {
val enclosingNode = node.parent
// erasing the check whether 'kotlinx-atomicfu' is defined
if (enclosingNode.type == Token.TYPEOF) {
if (enclosingNode.parent.parent.type == Token.IF) {
val ifStatement = enclosingNode.parent.parent as IfStatement
val falseKeyword = KeywordLiteral()
falseKeyword.type = Token.FALSE
ifStatement.condition = falseKeyword
val oneLineBlock = Block()
oneLineBlock.addStatement(EmptyLine())
ifStatement.thenPart = oneLineBlock
}
}
}
}
Token.BLOCK -> {
// erasing importsForInline for 'kotlinx-atomicfu'
for (stmt in node) {
if (stmt is ExpressionStatement) {
val expr = stmt.expression
if (expr is Assignment && expr.left is ElementGet) {
if (isAtomicfuDependency((expr.left as ElementGet).element)) {
node.replaceChild(stmt, EmptyLine())
}
}
}
}
}
}
return true
}
}
inner class AtomicConstructorDetector : NodeVisitor {
private fun kotlinxAtomicfuModuleName(name: String) = "$MODULE_KOTLINX_ATOMICFU.$KOTLINX_ATOMICFU_PACKAGE.$name"
override fun visit(node: AstNode?): Boolean {
if (node is Block) {
for (stmt in node) {
if (stmt is VariableDeclaration) {
val varInit = stmt.variables[0] as VariableInitializer
if (varInit.initializer is PropertyGet) {
val initializer = varInit.initializer.toSource()
if (initializer.matches(Regex(kotlinxAtomicfuModuleName("""($ATOMIC_CONSTRUCTOR|$ATOMIC_CONSTRUCTOR_BINARY_COMPATIBILITY)""")))) {
atomicConstructors.add(varInit.target.toSource())
node.replaceChild(stmt, EmptyLine())
} else if (initializer.matches(Regex(kotlinxAtomicfuModuleName(TRACE_CONSTRUCTOR)))) {
traceConstructors.add(varInit.target.toSource())
node.replaceChild(stmt, EmptyLine())
} else if (initializer.matches(Regex(kotlinxAtomicfuModuleName("""($LOCKS|$TRACE_FORMAT_CONSTRUCTOR|$TRACE_BASE_CLASS|$TRACE_NAMED)""")))) {
node.replaceChild(stmt, EmptyLine())
}
}
}
}
}
if (node is PropertyGet && node.property.toSource().matches(Regex(TRACE_FORMAT_FORMAT))) {
val target = node.target
node.property = Name().also { it.identifier = "emptyProperty" }
if (target is PropertyGet && target.property.toSource().matches(Regex(PROTOTYPE))) {
traceFormatObjects.add(target.target.toSource())
}
}
if (node is VariableInitializer && node.initializer is PropertyGet) {
val initializer = node.initializer.toSource()
if (initializer.matches(Regex(REENTRANT_LOCK_ATOMICFU_SINGLETON))) {
node.initializer = null
}
if (initializer.matches(Regex(kotlinxAtomicfuModuleName("""($ATOMIC_CONSTRUCTOR|$ATOMIC_CONSTRUCTOR_BINARY_COMPATIBILITY)""")))) {
atomicConstructors.add(node.target.toSource())
node.initializer = null
}
if (initializer.matches(Regex(kotlinxAtomicfuModuleName(ATOMIC_ARRAY_CONSTRUCTOR)))) {
val initialValue = when (initializer.substringAfterLast('$')) {
"int" -> "0"
"long" -> "0"
"boolean" -> "false"
else -> null
}
atomicArrayConstructors[node.target.toSource()] = initialValue
node.initializer = null
}
return false
} else if (node is Assignment && node.right is PropertyGet) {
val initializer = node.right.toSource()
if (initializer.matches(Regex(REENTRANT_LOCK_ATOMICFU_SINGLETON))) {
node.right = Name().also { it.identifier = "null" }
return false
}
}
return true
}
}
inner class FieldDelegatesVisitor : NodeVisitor {
override fun visit(node: AstNode?): Boolean {
if (node is FunctionCall) {
val functionName = node.target.toSource()
if (atomicConstructors.contains(functionName)) {
if (node.parent is Assignment) {
val assignment = node.parent as Assignment
val atomicField = assignment.left
val constructorBlock = ((node.parent.parent as? ExpressionStatement)?.parent as? Block)
?: abort("Incorrect tree structure of the constructor block initializing ${node.parent.toSource()}")
// check if there is a delegate field initialized by the reference to this atomic
for (stmt in constructorBlock) {
if (stmt is ExpressionStatement) {
if (stmt.expression is Assignment) {
val delegateAssignment = stmt.expression as Assignment
if (delegateAssignment.right is PropertyGet) {
val initializer = delegateAssignment.right as PropertyGet
if (initializer.toSource() == atomicField.toSource()) {
// register field delegate and the original atomic field
fieldDelegates[(delegateAssignment.left as PropertyGet).property.toSource()] =
(atomicField as PropertyGet).property.toSource()
}
}
}
}
}
}
}
}
return true
}
}
inner class DelegatedPropertyAccessorsVisitor : NodeVisitor {
override fun visit(node: AstNode?): Boolean {
if (node is PropertyGet) {
if (node.target is PropertyGet) {
if ((node.target as PropertyGet).property.toSource() in fieldDelegates && node.property.toSource() == MANGLED_VALUE_PROP) {
if (node.parent is ReturnStatement) {
val getter = ((((node.parent.parent as? Block)?.parent as? FunctionNode)?.parent as? ObjectProperty)?.parent as? ObjectLiteral)
?: abort("Incorrect tree structure of the accessor for the property delegated " +
"to the atomic field ${fieldDelegates[node.target.toSource()]}")
val definePropertyCall = getter.parent as FunctionCall
val stringLiteral = definePropertyCall.arguments[1] as? StringLiteral
?: abort ("Object.defineProperty invocation should take a property name as the second argument")
val delegatedProperty = stringLiteral.value.toString()
delegatedProperties[delegatedProperty] = (node.target as PropertyGet).property.toSource()
}
}
}
}
return true
}
}
inner class TransformVisitor : NodeVisitor {
override fun visit(node: AstNode): Boolean {
// remove atomic constructors from classes fields
if (node is FunctionCall) {
val functionName = node.target.toSource()
if (atomicConstructors.contains(functionName)) {
if (node.parent is Assignment) {
val valueNode = node.arguments[0]
(node.parent as Assignment).right = valueNode
}
return true
} else if (atomicArrayConstructors.contains(functionName)) {
val arrayConstructor = Name()
arrayConstructor.identifier = ARRAY
node.target = arrayConstructor
atomicArrayConstructors[functionName]?.let {
val arrayConsCall = FunctionCall()
arrayConsCall.target = node.target
arrayConsCall.arguments = node.arguments
val target = PropertyGet()
val fill = Name()
fill.identifier = FILL
target.target = arrayConsCall
target.property = fill
node.target = target
val initialValue = Name()
initialValue.identifier = it
node.arguments = listOf(initialValue)
}
return true
} else if (node.target is PropertyGet) {
if ((node.target as PropertyGet).target is FunctionCall) {
val atomicOperationTarget = node.target as PropertyGet
val funcCall = atomicOperationTarget.target as FunctionCall
if (funcCall.target is PropertyGet) {
val getterCall = (funcCall.target as PropertyGet).property
if (Regex(GET_ELEMENT).matches(getterCall.toSource())) {
val getter = getArrayElement(funcCall)
atomicOperationTarget.target = getter
}
}
}
}
}
// remove value property call
if (node is PropertyGet) {
if (node.property.toSource() == MANGLED_VALUE_PROP) {
// check whether atomic operation is performed on the type casted atomic field
node.target.eraseAtomicFieldFromUncheckedCast()?.let { node.target = it }
// A.a.value
if (node.target.type == Token.GETPROP) {
val clearField = node.target as PropertyGet
val targetNode = clearField.target
val clearProperety = clearField.property
node.setLeftAndRight(targetNode, clearProperety)
}
// other cases with $receiver.kotlinx$atomicfu$value in inline functions
else if (node.target.toSource().matches(Regex(RECEIVER))) {
val receiverName = node.target.toSource() // $receiver_i
val rr = ReceiverResolver(receiverName)
node.enclosingFunction.visit(rr)
rr.receiver?.let { node.target = it }
}
}
if (node.property.toSource() in delegatedProperties) {
// replace delegated property name with the name of the original atomic field
val fieldDelegate = delegatedProperties[node.property.toSource()]
val originalField = fieldDelegates[fieldDelegate]!!
node.property = Name().apply { identifier = originalField }
}
// replace Atomic*Array.size call with `length` property on the pure type js array
if (node.property.toSource() == ARRAY_SIZE) {
node.property = Name().also { it.identifier = LENGTH }
}
}
if (node is Block) {
for (stmt in node) {
if (stmt is ExpressionStatement) {
if (stmt.expression is Assignment) {
// erase field initialisation
val assignment = stmt.expression as Assignment
if (assignment.right is FunctionCall) {
val functionName = (assignment.right as FunctionCall).target.toSource()
if (traceConstructors.contains(functionName)) {
node.replaceChild(stmt, EmptyLine())
}
}
}
if (stmt.expression is FunctionCall) {
// erase append(text) call
val funcNode = (stmt.expression as FunctionCall).target
if (funcNode is PropertyGet && funcNode.property.toSource().matches(Regex(TRACE_APPEND))) {
node.replaceChild(stmt, EmptyLine())
}
}
}
}
}
if (node is Assignment && node.left is PropertyGet) {
val left = node.left as PropertyGet
if (traceFormatObjects.contains(left.target.toSource())) {
if (node.right is FunctionCall) {
// TraceFormatObject initialization
(node.right as FunctionCall).arguments = listOf(Name().also { it.identifier = "null" })
}
}
}
// remove TraceFormatObject constructor definition
if (node is FunctionNode && traceFormatObjects.contains(node.name)) {
val body = node.body
for (stmt in body) { body.replaceChild(stmt, EmptyLine()) }
}
// remove TraceFormat from TraceFormatObject interfaces
if (node is Assignment && node.left is PropertyGet && node.right is ObjectLiteral) {
val left = node.left as PropertyGet
val metadata = node.right as ObjectLiteral
if (traceFormatObjects.contains(left.target.toSource())) {
for (e in metadata.elements) {
if (e.right is ArrayLiteral) {
val array = (e.right as ArrayLiteral).toSource()
if (array.contains(TRACE_FORMAT)) {
(e.right as ArrayLiteral).elements = emptyList()
}
}
}
}
}
return true
}
private fun getArrayElement(getterCall: FunctionCall): AstNode {
val index = getterCall.arguments[0]
val arrayField = (getterCall.target as PropertyGet).target
// whether this field is static or not
val isStatic = arrayField !is PropertyGet
val arrName = if (isStatic) arrayField else (arrayField as PropertyGet).property
val getter = ElementGet(arrName, index)
return if (isStatic) { //intArr[index]
getter
} else { //A.intArr[0]
val call = PropertyGet()
call.target = (arrayField as PropertyGet).target
val name = Name()
name.identifier = getter.toSource()
call.property = name
call
}
}
}
// receiver data flow
inner class ReceiverResolver(private val receiverName: String) : NodeVisitor {
var receiver: AstNode? = null
override fun visit(node: AstNode): Boolean {
if (node is VariableInitializer) {
if (node.target.toSource() == receiverName) {
receiver = node.initializer
return false
}
}
return true
}
}
inner class AtomicOperationsInliner : NodeVisitor {
override fun visit(node: AstNode?): Boolean {
// inline atomic operations
if (node is FunctionCall) {
if (node.target is PropertyGet) {
val funcName = (node.target as PropertyGet).property
var field = (node.target as PropertyGet).target
if (field.toSource().matches(Regex(RECEIVER))) {
val receiverName = field.toSource() // $receiver_i
val rr = ReceiverResolver(receiverName)
node.enclosingFunction.visit(rr)
if (rr.receiver != null) {
field = rr.receiver
}
}
field.eraseAtomicFieldFromUncheckedCast()?.let { field = it }
val args = node.arguments
val inlined = node.inlineAtomicOperation(funcName.toSource(), field, args)
return !inlined
}
}
return true
}
}
private fun AstNode.eraseAtomicFieldFromUncheckedCast(): AstNode? {
if (this is ParenthesizedExpression && expression is ConditionalExpression) {
val testExpression = (expression as ConditionalExpression).testExpression
if (testExpression is FunctionCall && testExpression.target.toSource() == KOTLIN_TYPE_CHECK) {
// type check
val typeToCast = testExpression.arguments[1]
if ((typeToCast as Name).identifier == ATOMIC_REF) {
// unchecked type cast -> erase atomic field itself
return (testExpression.arguments[0] as Assignment).right
}
}
}
return null
}
private fun AstNode.isThisNode(): Boolean {
return when(this) {
is PropertyGet -> {
target.isThisNode()
}
is FunctionCall -> {
target.isThisNode()
}
else -> {
(this.type == Token.THIS)
}
}
}
private fun PropertyGet.resolvePropName(): String {
val target = this.target
return if (target is PropertyGet) {
"${target.resolvePropName()}.${property.toSource()}"
} else {
property.toSource()
}
}
private fun AstNode.scopedSource(): String {
if (this.isThisNode()) {
if (this is PropertyGet) {
val property = resolvePropName()
return "$SCOPE.$property"
} else if (this is FunctionCall && this.target is PropertyGet) {
// check that this function call is getting array element
if (this.target is PropertyGet) {
val funcName = (this.target as PropertyGet).property.toSource()
if (Regex(GET_ELEMENT).matches(funcName)) {
val property = (this.target as PropertyGet).resolvePropName()
return "$SCOPE.$property(${this.arguments[0].toSource()})"
}
}
} else if (this.type == Token.THIS) {
return SCOPE
}
}
return this.toSource()
}
private fun FunctionCall.inlineAtomicOperation(
funcName: String,
field: AstNode,
args: List<AstNode>
): Boolean {
val f = field.scopedSource()
val code = when (funcName) {
"atomicfu\$getAndSet" -> {
val arg = args[0].toSource()
"(function($SCOPE) {var oldValue = $f; $f = $arg; return oldValue;})()"
}
"atomicfu\$compareAndSet" -> {
val expected = args[0].scopedSource()
val updated = args[1].scopedSource()
val equals = if (expected == "null") "==" else "==="
"(function($SCOPE) {return $f $equals $expected ? function() { $f = $updated; return true }() : false})()"
}
"atomicfu\$getAndIncrement" -> {
"(function($SCOPE) {return $f++;})()"
}
"atomicfu\$getAndIncrement\$long" -> {
"(function($SCOPE) {var oldValue = $f; $f = $f.inc(); return oldValue;})()"
}
"atomicfu\$getAndDecrement" -> {
"(function($SCOPE) {return $f--;})()"
}
"atomicfu\$getAndDecrement\$long" -> {
"(function($SCOPE) {var oldValue = $f; $f = $f.dec(); return oldValue;})()"
}
"atomicfu\$getAndAdd" -> {
val arg = args[0].scopedSource()
"(function($SCOPE) {var oldValue = $f; $f += $arg; return oldValue;})()"
}
"atomicfu\$getAndAdd\$long" -> {
val arg = args[0].scopedSource()
"(function($SCOPE) {var oldValue = $f; $f = $f.add($arg); return oldValue;})()"
}
"atomicfu\$addAndGet" -> {
val arg = args[0].scopedSource()
"(function($SCOPE) {$f += $arg; return $f;})()"
}
"atomicfu\$addAndGet\$long" -> {
val arg = args[0].scopedSource()
"(function($SCOPE) {$f = $f.add($arg); return $f;})()"
}
"atomicfu\$incrementAndGet" -> {
"(function($SCOPE) {return ++$f;})()"
}
"atomicfu\$incrementAndGet\$long" -> {
"(function($SCOPE) {return $f = $f.inc();})()"
}
"atomicfu\$decrementAndGet" -> {
"(function($SCOPE) {return --$f;})()"
}
"atomicfu\$decrementAndGet\$long" -> {
"(function($SCOPE) {return $f = $f.dec();})()"
}
else -> null
}
if (code != null) {
this.setImpl(code)
return true
}
return false
}
private fun FunctionCall.setImpl(code: String) {
val p = Parser(CompilerEnvirons())
val node = p.parse(code, null, 0)
if (node.firstChild != null) {
val expr = (node.firstChild as ExpressionStatement).expression
this.target = (expr as FunctionCall).target
val thisNode = Parser(CompilerEnvirons()).parse("this", null, 0)
this.arguments = listOf((thisNode.firstChild as ExpressionStatement).expression)
}
}
}
private class EmptyLine : EmptyExpression() {
override fun toSource(depth: Int) = "\n"
}
fun main(args: Array<String>) {
if (args.size !in 1..2) {
println("Usage: AtomicFUTransformerKt <dir> [<output>]")
return
}
val t = AtomicFUTransformerJS(File(args[0]), File(args[1]))
t.transform()
}