Search code examples
javascala

Java class calling Scala 3 macro gets "bad class file", "undeclared type variable: B"


I'm having a problem with a Java-17 function calling a Scala 3.3.0 inline/macro based code and getting this error:

[error] /testDir/scala3-test/src/main/scala/csw/params/Test.java:7:1: cannot access io.bullet.borer.Encoder
[error]   bad class file: /home/user/.cache/coursier/v1/https/repo1.maven.org/maven2/io/bullet/borer-core_3/1.11.0/borer-core_3-1.11.0.jar(/io/bullet/borer/Encoder.class)
[error]     undeclared type variable: B
[error]     Please remove or make sure it appears in the correct subdirectory of the classpath.

I created a simple test repo that demonstrates the problem: https://github.com/abrighton/scala3-test

The Scala class:

import java.nio.charset.StandardCharsets
import io.bullet.borer.*
import io.bullet.borer.derivation.CompactMapBasedCodecs.deriveCodec
import play.api.libs.json.{Json => PJson, _}

object JsonSupport extends JsonSupport

trait JsonSupport {
  def writes[T: Encoder](x: T): JsValue = PJson.parse(Json.encode(x).toUtf8String)
  def reads[T: Decoder](x: JsValue): T  = Json.decode(x.toString().getBytes(StandardCharsets.UTF_8)).to[T].value
}

case class ProperMotion(pmx: Double, pmy: Double)

object ProperMotion {
  implicit val properMotionCodec: Codec[ProperMotion] = deriveCodec
}

The Java class:

public class Test {

    public void shouldConvertPmToFromJSON() {
        var pm = new ProperMotion(0.5, 2.33);
        var pmjs = JsonSupport.writes(pm, ProperMotion.properMotionCodec().encoder());
    }
}

And deriveCodec (from borer) looks like this:

  inline def deriveCodec[T]: Codec[T] = Codec(deriveEncoder[T], deriveDecoder[T])

The class file is not corrupted (I have tested with multiple versions). This seems to be an issue with Java calling an inline Scala function.

The Scala version compiles:

class TestX {

  def shouldConvertPmToFromJSON(): Unit = {
    val pm = ProperMotion(0.5, 2.33)
    val pmjs = JsonSupport.writes(pm)
  }
}

This type of code compiled previously under Scala-2.13.


Solution

  • Scala macros cannot be called from Java.

    When you have:

    // Scala 2
    def method: Foo = macro Macros.methodImpl
    

    or

    // Scala 3
    inline def method = ${ Macros.metodImpl }
    

    basically compiler will create in the bytecode a method with an empty body that cannot be called. But that method will have some extra bytecode properties which will tell Scala compiler which other method could take its arguments converted to Expr type, its type parameters as Type/WeakTypeTag and return Expr with the AST of the body.

    None of these abstraction are readable by Java compiler. For javac it's a malformed method (or method implementing throw new NotYetImplemented) with unknown attributes.

    I have no idea how it was supposed to work in Scala 2.13. The last time I checked macro-methods had some macro attributes and were implemented as throw new NotYetImplemented which could be called (from Java or through runtime reflection) but would not return anything meaningful.