Context: For some tests I had to return a Promise ...
# compiler
v
Context: For some tests I had to return a Promise from test method. I had written a multiplatform (expect/actual)
kmpTestPromise
function to make tests multiplatform. This function return Promise on JS-target (to make test framework process it) and process promise inside and return null on other platforms (which has multiple threads). Later it turned out that tests that return non-Unit results are not supported on all targeters except JS. That's when I decided that this is a good case for my first compiler plugin.... The question. I've written a compiler plugin, which goal is to make test function return Unit on JVM and Native and keep it as is on JS. For example it must convert this function:
Copy code
@Test
fun should_test_async(): Any? {
  println("test")  
  return kmpTestPromise { resolve, reject -> // (1)
    resolve(null)
  }
}
into this function:
Copy code
@Test
fun should_test_async(): Unit {
  println("test")
  kmpTestPromise { resolve, reject ->
    resolve(null)
  }
  return
}
on native and jvm. The signature of kmpTestPromise:
Copy code
expect fun kmpTestPromise(
    block: (resolve: (Any?) -> Unit, reject: (Throwable) -> Unit) -> Any?
): Any?
The transformation itself takes place in
IrElementTransformerVoidWithContext::visitReturn
implementation:
Copy code
private class ReturnsToUnitTransformer(
  private val function: IrFunction, 
  private val pluginContext: IrPluginContext
): IrElementTransformerVoidWithContext() {
  override fun visitReturn(returnExpression: IrReturn): IrExpression {
    // ignore irrelevant returns (return@let, return@launch, etc.)
    if (returnExpression.returnTargetSymbol != function.symbol) return returnExpression 

    return DeclarationIrBuilder(
      pluginContext, 
      function.symbol, 
      returnExpression.startOffset, 
      returnExpression.endOffset
    ).irBlock {
        +returnExpression.value // (2) It seems this operation is not fully correct.
        +DeclarationIrBuilder(
            pluginContext,
            function.symbol,
            returnExpression.startOffset,
            returnExpression.endOffset
        ).irReturnUnit()
    }
  }
}
It applyed inside
IrElementTransformerVoidWithContext
(I've tried
IrElementVisitorVoid
as well. Nothing changed). Compiler plugin works as expected on native target, but generates broken code on JVM. On decompiled code I can see, that expression
(1)
is changed during
(2)
operation. It's param become null instead of original lambda:
Copy code
@Test
public final void should_test_async() {
  System.out.println("test");
  KmKt.kmpTestPromise((Function2)null.INSTANCE); // null is not expected to be here.
}
The test runtime exception is:
Copy code
java.lang.NoSuchMethodError: 
'java.lang.Object com.exl.KmKt.kmpTestPromise(kotlin.jvm.functions.Function2)'
Am I missing something as a newbie in compiler plugin development? Or maybe it's a bug? Different results on Native (works fine) and JVM(generates broken code) confuse me. I use kotlin 1.9.10. I've tried to use plugin on K2 - no difference. Full reproducer: https://github.com/vdshb/kcp-reproducer
s
I am not sure what causes the failure on compiler side, but I am not sure you actually need the plugin It should be possible to just return Unit from actual, I believe?
v
I'm testing external process interaction. It's a chain MyAction-ProcessReaction-MyAction-ProcessReaction-... . On JS I have to release the working thread to get every next process reaction. I found creating a promise-chain the only way to test it.
s
Right, but you can define actual for kmpPromise function that always returns unit on other platforms, I believe