Should extension methods works with Dynamic trait?

I would like the following code works:

@main
def test1(): Unit = {
    val a = Some("hello")

    val b = a.!.length
    val c = a.!.toUpperCase
    
    println(b)
    println(c)
}

I can implements the ! as:

import scala.language.dynamics

implicit final class Boxed[T](val a: T) extends AnyVal, Dynamic:
    inline transparent def selectDynamic(inline name: String): Any = ${ BoxedMacro.selectDynamicImpl[T]('{ a }, '{ name } )}

opaque type OpaueBox[T] = T
extension [T](a: OpaueBox[T])
    inline def selectDynamic(inline name: String): Any = ${ BoxedMacro.selectDynamicImpl[T]('{ a }, '{ name } )}

object BoxedMacro:

    import scala.quoted.*
    
    // a demo implementation for fast test 
    def selectDynamicImpl[T: Type](container: Expr[T], selector: Expr[String])(using Quotes): Expr[Any] =
        import quotes.reflect.*

        // container.map(_.selector)
        val container2: Expr[Option[String]] = container.asExprOf[Option[String]]
        selector.value match
            case Some("length") =>
                '{ $container2.map(x => x.length) }
            case Some("toUpperCase") =>
                '{ $container2.map(x => x.toUpperCase) }
            case Some(name@_) =>
                report.error(s"selector ${name} Not found")
                '{ None }
            case None =>
                report.error(s"selector is not a constant")
                '{ None }


extension [T](a: Option[T])
    inline def ! : Boxed[Option[T]] = Boxed(a)  // this works
    // inline def ! : OpaueBox[Option[T]] = a  // this does not work

by using the AnyVal implicit, it works, but the generate byte code is something complex, it called a empty Boxed method and checkcast multi times.

      12: aload_1
      13: invokevirtual #51                 // Method Main$package$.Boxed:(Ljava/lang/Object;)Ljava/lang/Object;  
      16: checkcast     #53                 // class scala/Option
      19: astore_3
      20: aload_3
      21: checkcast     #53                 // class scala/Option

       // $3 = $1, so bytecode 12..21 is redundancy code 

      24: invokedynamic #72,  0             // InvokeDynamic #0:apply:()Lscala/Function1;
      29: invokevirtual #76                 // Method scala/Option.map:(Lscala/Function1;)Lscala/Option;
      32: astore_2

I world like to using the opaque type which has no runtime cost, but it looks the extension not works with Dynamic method(selectDynamic).

So, My question is: Can Scala3 support extension methods works with Dynamic trait? If it can’t, Why it can’t?

2 Likes

I don’t think that class Boxed has to be an implicit class for it to work.

With a little hack I can make the opaque type work. But perhaps that partly defeats the purpose of an opaque type… And I’m not sure that it will make much difference in the bytecode.

Welcome to Scala 3.3.1 (21.0.2, Java OpenJDK 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.

scala> object Test:
     |   opaque type Box[A] <: Dynamic = A & Dynamic
     |   extension [A](box: Box[A])
     |     inline def selectDynamic(name: String): String = "calling " + name
     |
     |   extension [A](a: A)
     |     inline def ! : Box[A] = a.asInstanceOf[A & Dynamic]
     |
// defined object Test

scala> import Test.*

scala> Option(3).!.foo
val res0: String = calling foo

[quote=“Jasper-M, post:2, topic:6496”]

Your solution works well. and I check the bytecode, there is still some redundant code, but it looks this code can be easily erased in JIT step.

    val a = Some("hello")

    val b: Option[Int] = a.!.length

       0: getstatic     #36                 // Field scala/Some$.MODULE$:Lscala/Some$;
       3: ldc           #38                 // String hello
       5: invokevirtual #42                 // Method scala/Some$.apply:(Ljava/lang/Object;)Lscala/Some;
       8: astore_1                        // $1 = Some("hello")

       9: getstatic     #47                 // Field Box$.MODULE$:LBox$;
      12: astore_3
      13: getstatic     #47                 // Field Box$.MODULE$:LBox$;
      16: astore        5
      18: aload         5
      20: astore        6

      // 9..20 is redundant code, which maybe erased by JIT.

      22: aload_1
      23: astore        4
      25: aload         4
      27: invokedynamic #67,  0             // InvokeDynamic #0:apply:()Lscala/Function1;
      32: invokevirtual #73                 // Method scala/Some.map:(Lscala/Function1;)Lscala/Option;
      35: astore_2.                             // $2 = $1.map( f )

there is no checkcast bytecode.