JS: implementing try/finally in coroutines

This commit is contained in:
Alexey Andreev
2016-11-01 19:22:17 +03:00
parent 23bddac4fe
commit d5a808dff6
3 changed files with 250 additions and 61 deletions
@@ -2,7 +2,7 @@
var globalResult = "" var globalResult = ""
var wasCalled = false var wasCalled = false
class Controller { class Controller {
val postponedActions = java.util.ArrayList<() -> Unit>() val postponedActions = mutableListOf<() -> Unit>()
suspend fun suspendWithValue(v: String, x: Continuation<String>) { suspend fun suspendWithValue(v: String, x: Continuation<String>) {
postponedActions.add { postponedActions.add {
@@ -71,7 +71,7 @@ fun box(): String {
builder(expectException = true) { builder(expectException = true) {
try { try {
suspendWithException(java.lang.RuntimeException("OK")) suspendWithException(RuntimeException("OK"))
} finally { } finally {
if (suspendWithValue("G") != "G") throw RuntimeException("fail 2") if (suspendWithValue("G") != "G") throw RuntimeException("fail 2")
wasCalled = true wasCalled = true
@@ -30,23 +30,32 @@ class CoroutineBodyTransformer(val program: JsProgram, val scope: JsScope, val t
private val currentStatements: MutableList<JsStatement> private val currentStatements: MutableList<JsStatement>
get() = currentBlock.statements get() = currentBlock.statements
val resultFieldName by lazy { scope.declareFreshName("\$result") } val resultFieldName by lazy { scope.declareFreshName("\$result") }
val exceptionFieldName by lazy { scope.declareFreshName("\$exception") }
val stateFieldName by lazy { scope.declareFreshName("\$state") } val stateFieldName by lazy { scope.declareFreshName("\$state") }
val controllerFieldName by lazy { scope.declareFreshName("\$controller") } val controllerFieldName by lazy { scope.declareFreshName("\$controller") }
val exceptionStateName by lazy { scope.declareFreshName("\$exceptionState") } val exceptionStateName by lazy { scope.declareFreshName("\$exceptionState") }
private val breakTargets = mutableMapOf<JsName, JsStatement>() val finallyPathFieldName by lazy { scope.declareFreshName("\$finallyPath") }
private val continueTargets = mutableMapOf<JsName, JsStatement>() private val breakTargets = mutableMapOf<JsName, JumpTarget>()
private var defaultBreakTarget: JsStatement = JsEmpty private val continueTargets = mutableMapOf<JsName, JumpTarget>()
private var defaultContinueTarget: JsStatement = JsEmpty private var defaultBreakTarget: JumpTarget = JumpTarget(JsEmpty, 0)
private var defaultContinueTarget: JumpTarget = JumpTarget(JsEmpty, 0)
private val referencedBlocks = mutableSetOf<CoroutineBlock>() private val referencedBlocks = mutableSetOf<CoroutineBlock>()
private val breakBlocks = mutableMapOf<JsStatement, CoroutineBlock>() private val breakBlocks = mutableMapOf<JsStatement, CoroutineBlock>()
private val continueBlocks = mutableMapOf<JsStatement, CoroutineBlock>() private val continueBlocks = mutableMapOf<JsStatement, CoroutineBlock>()
private lateinit var nodesToSplit: Set<JsNode> private lateinit var nodesToSplit: Set<JsNode>
private var currentCatchBlock = globalCatchBlock private var currentCatchBlock = globalCatchBlock
private val tryStack = mutableListOf(TryBlock(globalCatchBlock, null))
var hasFinallyBlocks = false
get
private set
private val currentTryDepth = tryStack.lastIndex
fun preProcess(node: JsNode) { fun preProcess(node: JsNode) {
val nodes = mutableSetOf<JsNode>() val nodes = mutableSetOf<JsNode>()
node.accept(object : RecursiveJsVisitor() { val visitor = object : RecursiveJsVisitor() {
var childrenInSet = false var childrenInSet = false
override fun visitInvocation(invocation: JsInvocation) { override fun visitInvocation(invocation: JsInvocation) {
@@ -57,10 +66,33 @@ class CoroutineBodyTransformer(val program: JsProgram, val scope: JsScope, val t
} }
} }
override fun visitReturn(x: JsReturn) {
super.visitReturn(x)
nodes += x
childrenInSet = true
}
override fun visitBreak(x: JsBreak) {
super.visitBreak(x)
// It's a simplification
// TODO: don't split break and continue statements when possible
nodes += x
childrenInSet = true
}
override fun visitContinue(x: JsContinue) {
super.visitContinue(x)
nodes += x
childrenInSet = true
}
override fun visitElement(node: JsNode) { override fun visitElement(node: JsNode) {
val oldChildrenInSet = childrenInSet val oldChildrenInSet = childrenInSet
childrenInSet = false childrenInSet = false
node.acceptChildren(this) node.acceptChildren(this)
if (childrenInSet) { if (childrenInSet) {
nodes += node nodes += node
} }
@@ -68,16 +100,20 @@ class CoroutineBodyTransformer(val program: JsProgram, val scope: JsScope, val t
childrenInSet = oldChildrenInSet childrenInSet = oldChildrenInSet
} }
} }
}) }
visitor.accept(node)
visitor.accept(node)
nodesToSplit = nodes nodesToSplit = nodes
currentStatements += exceptionState(currentCatchBlock)
} }
fun postProcess(): List<CoroutineBlock> { fun postProcess(): List<CoroutineBlock> {
if (entryBlock == currentBlock) return listOf(entryBlock)
currentBlock.statements += JsReturn() currentBlock.statements += JsReturn()
val orderedBlocks = DFS.topologicalOrder(listOf(entryBlock)) { it.collectTargetBlocks() } val graph = buildBlockGraph()
val orderedBlocks = DFS.topologicalOrder(listOf(entryBlock)) { graph[it].orEmpty() }
val blockIndexes = orderedBlocks.withIndex().associate { (index, block) -> Pair(block, index) } val blockIndexes = orderedBlocks.withIndex().associate { (index, block) -> Pair(block, index) }
val blockReplacementVisitor = object : JsVisitorWithContextImpl() { val blockReplacementVisitor = object : JsVisitorWithContextImpl() {
@@ -95,6 +131,18 @@ class CoroutineBodyTransformer(val program: JsProgram, val scope: JsScope, val t
val rhs = program.getNumberLiteral(blockIndexes[exceptionTarget]!!) val rhs = program.getNumberLiteral(blockIndexes[exceptionTarget]!!)
ctx.replaceMe(JsAstUtils.assignment(lhs, rhs).makeStmt()) ctx.replaceMe(JsAstUtils.assignment(lhs, rhs).makeStmt())
} }
val finallyPath = x.finallyPath
if (finallyPath != null) {
if (finallyPath.isNotEmpty()) {
val lhs = JsNameRef(finallyPathFieldName, JsLiteral.THIS)
val rhs = JsArrayLiteral(finallyPath.map { program.getNumberLiteral(blockIndexes[it]!!) })
ctx.replaceMe(JsAstUtils.assignment(lhs, rhs).makeStmt())
}
else {
ctx.removeMe()
}
}
} }
} }
for (block in orderedBlocks) { for (block in orderedBlocks) {
@@ -104,6 +152,35 @@ class CoroutineBodyTransformer(val program: JsProgram, val scope: JsScope, val t
return orderedBlocks return orderedBlocks
} }
private fun buildBlockGraph(): Map<CoroutineBlock, Set<CoroutineBlock>> {
// That's a little more than DFS due to need of tracking finally paths
val visitedBlocks = mutableSetOf<CoroutineBlock>()
val graph = mutableMapOf<CoroutineBlock, MutableSet<CoroutineBlock>>()
fun visitBlock(block: CoroutineBlock) {
if (block in visitedBlocks) return
for (finallyPath in block.collectFinallyPaths()) {
for ((finallySource, finallyTarget) in (listOf(block) + finallyPath).zip(finallyPath)) {
if (graph.getOrPut(finallySource) { mutableSetOf() }.add(finallyTarget)) {
visitedBlocks -= finallySource
}
}
}
visitedBlocks += block
val successors = graph.getOrPut(block) { mutableSetOf() }
successors += block.collectTargetBlocks()
successors.forEach(::visitBlock)
}
visitBlock(entryBlock)
return graph
}
override fun visitBlock(x: JsBlock) = splitIfNecessary(x) { override fun visitBlock(x: JsBlock) = splitIfNecessary(x) {
for (statement in x.statements) { for (statement in x.statements) {
statement.accept(this) statement.accept(this)
@@ -146,7 +223,9 @@ class CoroutineBodyTransformer(val program: JsProgram, val scope: JsScope, val t
else -> splitIfNecessary(x) { else -> splitIfNecessary(x) {
val successor = CoroutineBlock() val successor = CoroutineBlock()
accept(inner) withBreakAndContinue(x.name, x.statement, successor, null) {
accept(inner)
}
if (successor in referencedBlocks) { if (successor in referencedBlocks) {
currentBlock.statements += stateAndJump(successor) currentBlock.statements += stateAndJump(successor)
currentBlock = successor currentBlock = successor
@@ -237,68 +316,121 @@ class CoroutineBodyTransformer(val program: JsProgram, val scope: JsScope, val t
override fun visitBreak(x: JsBreak) { override fun visitBreak(x: JsBreak) {
val targetLabel = x.label?.name val targetLabel = x.label?.name
val targetStatement = if (targetLabel == null) { val (targetStatement, targetTryDepth) = if (targetLabel == null) {
defaultBreakTarget defaultBreakTarget
} }
else { else {
breakTargets[targetLabel]!! breakTargets[targetLabel]!!
} }
if (targetStatement in nodesToSplit) { val targetBlock = breakBlocks[targetStatement]!!
val targetBlock = breakBlocks[targetStatement]!! referencedBlocks += targetBlock
referencedBlocks += targetBlock jumpWithFinally(targetTryDepth, targetBlock)
currentStatements += stateAndJump(targetBlock) currentStatements += jump()
}
else {
currentStatements += x
}
} }
override fun visitContinue(x: JsContinue) { override fun visitContinue(x: JsContinue) {
val targetLabel = x.label?.name val targetLabel = x.label?.name
val targetStatement = if (targetLabel == null) { val (targetStatement, targetTryDepth) = if (targetLabel == null) {
defaultContinueTarget defaultContinueTarget
} }
else { else {
continueTargets[targetLabel]!! continueTargets[targetLabel]!!
} }
if (targetStatement in nodesToSplit) { val targetBlock = continueBlocks[targetStatement]!!
val targetBlock = continueBlocks[targetStatement]!! referencedBlocks += targetBlock
referencedBlocks += targetBlock
currentStatements += stateAndJump(targetBlock) jumpWithFinally(targetTryDepth, targetBlock)
} currentStatements += jump()
else { }
currentStatements += x
private fun jumpWithFinally(targetTryDepth: Int, successor: CoroutineBlock) {
if (targetTryDepth == tryStack.size) return
val tryBlock = tryStack[targetTryDepth]
currentStatements += exceptionState(tryBlock.catchBlock)
val relativeFinallyPath = relativeFinallyPath(targetTryDepth)
val fullPath = relativeFinallyPath + successor
if (fullPath.size > 1) {
currentStatements += updateFinallyPath(fullPath.drop(1))
} }
currentStatements += state(fullPath[0])
} }
override fun visitTry(x: JsTry) = splitIfNecessary(x) { override fun visitTry(x: JsTry) = splitIfNecessary(x) {
val catch = x.catches.firstOrNull() val catchNode = x.catches.firstOrNull()
val finallyNode = x.finallyBlock
val successor = CoroutineBlock() val successor = CoroutineBlock()
val catchBlock = CoroutineBlock() val catchBlock = CoroutineBlock()
val finallyBlock = CoroutineBlock()
val tryBlock = TryBlock(catchBlock, if (finallyNode != null) finallyBlock else null)
tryStack += tryBlock
val oldCatchBlock = currentCatchBlock val oldCatchBlock = currentCatchBlock
if (catch != null) { currentCatchBlock = catchBlock
currentCatchBlock = catchBlock currentStatements += exceptionState(catchBlock)
}
x.tryBlock.statements.forEach { it.accept(this) } x.tryBlock.statements.forEach { it.accept(this) }
currentBlock.statements += stateAndJump(successor) currentStatements += exceptionState(oldCatchBlock)
if (finallyNode != null) {
currentStatements += updateFinallyPath(listOf(successor))
currentStatements += stateAndJump(finallyBlock)
}
else {
currentStatements += stateAndJump(successor)
}
currentCatchBlock = oldCatchBlock currentCatchBlock = oldCatchBlock
if (catch != null) { // Handle catch node
currentBlock = catchBlock currentBlock = catchBlock
currentBlock.statements += JsAstUtils.newVar(catch.parameter.name, JsNameRef(resultFieldName, JsLiteral.THIS))
catch.body.statements.forEach { it.accept(this) } if (finallyNode != null) {
currentBlock.statements += stateAndJump(successor) currentStatements += updateFinallyPath(listOf(oldCatchBlock))
currentStatements += exceptionState(finallyBlock)
}
else {
currentStatements += exceptionState(oldCatchBlock)
} }
if (catchNode != null) {
currentStatements += JsAstUtils.newVar(catchNode.parameter.name, JsNameRef(exceptionFieldName, JsLiteral.THIS))
catchNode.body.statements.forEach { it.accept(this) }
if (finallyNode == null) {
currentStatements += stateAndJump(successor)
}
else {
currentStatements += updateFinallyPath(listOf(successor))
currentStatements += stateAndJump(finallyBlock)
}
}
// Handle finally node
if (finallyNode != null) {
currentBlock = finallyBlock
finallyNode.statements.forEach { it.accept(this) }
generateFinallyExit()
hasFinallyBlocks = true
}
tryStack.removeAt(tryStack.lastIndex)
currentBlock = successor currentBlock = successor
} }
private fun generateFinallyExit() {
val finallyPathRef = JsNameRef(finallyPathFieldName, JsLiteral.THIS)
val stateRef = JsNameRef(stateFieldName, JsLiteral.THIS)
val nextState = JsInvocation(JsNameRef("shift", finallyPathRef))
currentStatements += JsAstUtils.assignment(stateRef, nextState).makeStmt()
currentStatements += jump()
}
override fun visitExpressionStatement(x: JsExpressionStatement) { override fun visitExpressionStatement(x: JsExpressionStatement) {
val expression = x.expression val expression = x.expression
if (expression is JsInvocation && expression.isSuspend) { if (expression is JsInvocation && expression.isSuspend) {
@@ -323,11 +455,26 @@ class CoroutineBodyTransformer(val program: JsProgram, val scope: JsScope, val t
} }
override fun visitReturn(x: JsReturn) { override fun visitReturn(x: JsReturn) {
val returnBlock = CoroutineBlock()
val isInFinally = hasEnclosingFinallyBlock()
if (isInFinally) {
jumpWithFinally(0, returnBlock)
}
val returnExpression = x.expression val returnExpression = x.expression
if (returnExpression != null) { if (returnExpression != null) {
x.expression = handleExpression(returnExpression) x.expression = handleExpression(returnExpression)
} }
currentStatements += x
if (isInFinally) {
currentStatements += x.expression?.makeStmt().singletonOrEmptyList()
currentStatements += jump()
currentBlock = returnBlock
currentStatements += JsReturn()
}
else {
currentStatements += x
}
} }
override fun visitThrow(x: JsThrow) { override fun visitThrow(x: JsThrow) {
@@ -373,13 +520,10 @@ class CoroutineBodyTransformer(val program: JsProgram, val scope: JsScope, val t
} }
private fun state(target: CoroutineBlock): List<JsStatement> { private fun state(target: CoroutineBlock): List<JsStatement> {
val nextPlaceholder = JsDebugger() val placeholder = JsDebugger()
nextPlaceholder.targetBlock = target placeholder.targetBlock = target
val exceptionPlaceholder = JsDebugger() return listOf(placeholder)
exceptionPlaceholder.targetExceptionBlock = currentCatchBlock
return listOf(nextPlaceholder, exceptionPlaceholder)
} }
private fun jump() = JsContinue() private fun jump() = JsContinue()
@@ -388,6 +532,19 @@ class CoroutineBodyTransformer(val program: JsProgram, val scope: JsScope, val t
return state(target) + jump() return state(target) + jump()
} }
private fun exceptionState(target: CoroutineBlock): List<JsStatement> {
val placeholder = JsDebugger()
placeholder.targetExceptionBlock = target
return listOf(placeholder)
}
private fun updateFinallyPath(path: List<CoroutineBlock>): List<JsStatement> {
val placeholder = JsDebugger()
placeholder.finallyPath = path
return listOf(placeholder)
}
private inline fun splitIfNecessary(statement: JsStatement, action: () -> Unit) { private inline fun splitIfNecessary(statement: JsStatement, action: () -> Unit) {
if (statement in nodesToSplit) { if (statement in nodesToSplit) {
action() action()
@@ -418,15 +575,15 @@ class CoroutineBodyTransformer(val program: JsProgram, val scope: JsScope, val t
Pair(null, null) Pair(null, null)
} }
defaultBreakTarget = statement defaultBreakTarget = JumpTarget(statement, currentTryDepth)
if (label != null) { if (label != null) {
breakTargets[label] = statement breakTargets[label] = JumpTarget(statement, currentTryDepth)
if (continueBlock != null) { if (continueBlock != null) {
continueTargets[label] = statement continueTargets[label] = JumpTarget(statement, currentTryDepth)
} }
} }
if (continueBlock != null) { if (continueBlock != null) {
defaultContinueTarget = statement defaultContinueTarget = JumpTarget(statement, currentTryDepth)
} }
action() action()
@@ -448,8 +605,16 @@ class CoroutineBodyTransformer(val program: JsProgram, val scope: JsScope, val t
} }
} }
} }
private fun relativeFinallyPath(targetTryDepth: Int) = tryStack.subList(targetTryDepth, tryStack.size).mapNotNull { it.finallyBlock }
private fun hasEnclosingFinallyBlock() = tryStack.any { it.finallyBlock != null }
} }
private data class JumpTarget(val statement: JsStatement, val tryDepth: Int)
private class TryBlock(val catchBlock: CoroutineBlock, val finallyBlock: CoroutineBlock?)
private fun CoroutineBlock.collectTargetBlocks(): Set<CoroutineBlock> { private fun CoroutineBlock.collectTargetBlocks(): Set<CoroutineBlock> {
val targetBlocks = mutableSetOf<CoroutineBlock>() val targetBlocks = mutableSetOf<CoroutineBlock>()
jsBlock.accept(object : RecursiveJsVisitor() { jsBlock.accept(object : RecursiveJsVisitor() {
@@ -460,5 +625,16 @@ private fun CoroutineBlock.collectTargetBlocks(): Set<CoroutineBlock> {
return targetBlocks return targetBlocks
} }
private fun CoroutineBlock.collectFinallyPaths(): List<List<CoroutineBlock>> {
val finallyPaths = mutableListOf<List<CoroutineBlock>>()
jsBlock.accept(object : RecursiveJsVisitor() {
override fun visitDebugger(x: JsDebugger) {
x.finallyPath?.let { finallyPaths += it }
}
})
return finallyPaths
}
private var JsDebugger.targetBlock: CoroutineBlock? by MetadataProperty(default = null) private var JsDebugger.targetBlock: CoroutineBlock? by MetadataProperty(default = null)
private var JsDebugger.targetExceptionBlock: CoroutineBlock? by MetadataProperty(default = null) private var JsDebugger.targetExceptionBlock: CoroutineBlock? by MetadataProperty(default = null)
private var JsDebugger.finallyPath: List<CoroutineBlock>? by MetadataProperty(default = null)
@@ -98,6 +98,10 @@ class CoroutineFunctionTransformer(
constructor.body.statements.run { constructor.body.statements.run {
assign(bodyTransformer.stateFieldName, program.getNumberLiteral(0)) assign(bodyTransformer.stateFieldName, program.getNumberLiteral(0))
assign(bodyTransformer.exceptionStateName, program.getNumberLiteral(0))
if (bodyTransformer.hasFinallyBlocks) {
assign(bodyTransformer.finallyPathFieldName, JsLiteral.NULL)
}
assign(bodyTransformer.controllerFieldName, controllerName.makeRef()) assign(bodyTransformer.controllerFieldName, controllerName.makeRef())
for (localVariable in localVariables) { for (localVariable in localVariables) {
val value = if (localVariable !in parameterNames) JsLiteral.NULL else localVariable.makeRef() val value = if (localVariable !in parameterNames) JsLiteral.NULL else localVariable.makeRef()
@@ -185,13 +189,24 @@ class CoroutineFunctionTransformer(
throwName: JsName?, throwName: JsName?,
exceptionName: JsName exceptionName: JsName
): List<JsStatement> { ): List<JsStatement> {
val indexOfGlobalCatch = blocks.indexOf(transformer.globalCatchBlock)
val stateRef = JsNameRef(transformer.stateFieldName, JsLiteral.THIS)
val isFromGlobalCatch = JsAstUtils.equality(stateRef, program.getNumberLiteral(indexOfGlobalCatch))
val catch = JsCatch(functionWithBody.scope, "e") val catch = JsCatch(functionWithBody.scope, "e")
val continueWithException = JsBlock(
JsAstUtils.assignment(stateRef.deepCopy(), JsNameRef(transformer.exceptionStateName, JsLiteral.THIS)).makeStmt(),
JsAstUtils.assignment(JsNameRef(transformer.exceptionFieldName, JsLiteral.THIS), catch.parameter.name.makeRef()).makeStmt()
)
catch.body = JsBlock(JsIf(isFromGlobalCatch, JsThrow(catch.parameter.name.makeRef()), continueWithException))
val throwResultRef = JsNameRef(transformer.exceptionFieldName, JsLiteral.THIS)
if (throwName != null) { if (throwName != null) {
val throwMethodRef = JsNameRef(throwName, JsNameRef(transformer.controllerFieldName, JsLiteral.THIS)) val throwMethodRef = JsNameRef(throwName, JsNameRef(transformer.controllerFieldName, JsLiteral.THIS))
catch.body = JsBlock(JsReturn(JsInvocation(throwMethodRef, catch.parameter.name.makeRef()))) transformer.globalCatchBlock.statements += JsReturn(JsInvocation(throwMethodRef.deepCopy(), throwResultRef))
}
val resultRef = JsNameRef(transformer.resultFieldName, JsLiteral.THIS) else {
transformer.globalCatchBlock.statements += JsReturn(JsInvocation(throwMethodRef.deepCopy(), resultRef)) transformer.globalCatchBlock.statements += JsThrow(throwResultRef)
} }
val cases = blocks.withIndex().map { (index, block) -> val cases = blocks.withIndex().map { (index, block) ->
@@ -200,20 +215,18 @@ class CoroutineFunctionTransformer(
statements += block.statements statements += block.statements
} }
} }
val stateRef = JsNameRef(transformer.stateFieldName, JsLiteral.THIS) val switchStatement = JsSwitch(stateRef.deepCopy(), cases)
val switchStatement = JsSwitch(stateRef, cases) val loop = JsDoWhile(JsLiteral.TRUE, JsTry(JsBlock(switchStatement), catch, null))
val loop = JsDoWhile(JsLiteral.TRUE, switchStatement)
val testExceptionPassed = JsAstUtils.notOptimized( val testExceptionPassed = JsAstUtils.notOptimized(
JsAstUtils.typeOfIs(exceptionName.makeRef(), program.getStringLiteral("undefined"))) JsAstUtils.typeOfIs(exceptionName.makeRef(), program.getStringLiteral("undefined")))
val stateToException = JsAstUtils.assignment( val stateToException = JsAstUtils.assignment(
JsNameRef(transformer.stateFieldName, JsLiteral.THIS), JsNameRef(transformer.stateFieldName, JsLiteral.THIS),
JsNameRef(transformer.exceptionStateName, JsLiteral.THIS)) JsNameRef(transformer.exceptionStateName, JsLiteral.THIS))
val exceptionToResult = JsAstUtils.assignment(JsNameRef(transformer.resultFieldName, JsLiteral.THIS), exceptionName.makeRef()) val exceptionToResult = JsAstUtils.assignment(JsNameRef(transformer.exceptionFieldName, JsLiteral.THIS), exceptionName.makeRef())
val throwExceptionIfNeeded = JsIf(testExceptionPassed, JsBlock(stateToException.makeStmt(), exceptionToResult.makeStmt())) val throwExceptionIfNeeded = JsIf(testExceptionPassed, JsBlock(stateToException.makeStmt(), exceptionToResult.makeStmt()))
val resultBlock = JsBlock(throwExceptionIfNeeded, loop) return listOf(throwExceptionIfNeeded, loop)
return if (throwName != null) listOf(JsTry(resultBlock, catch, null)) else resultBlock.statements
} }
private fun JsBlock.replaceHandleResult(transformer: CoroutineBodyTransformer) { private fun JsBlock.replaceHandleResult(transformer: CoroutineBodyTransformer) {