Skip to content

Unexpected macro argument type substitution on the method owner change in the Symbol.newClass call #15924

Closed
@pomadchin

Description

@pomadchin

Compiler version

  • 3.2.0-RC4 / main

Minimized code

Note: mb I'm writing a wrong code :/

Usage:

trait Foo[F[_]]:
  def foo(id: F[Int]): Unit
  
// multiple times

// macroMinimized2.derive creates an instance of Foo with the substituted type param, i.e.:
// new Foo[Option] { def foo(id: Option[Int]): Unit = ... } 
// new Foo[Try] { def foo(id: Try[Int]): Unit = ... }

// however more than a single call fails
macroMinimized2.derive[Foo, Option]
macroMinimized2.derive[Foo, Try]

Macro code (I tried to minimize it as much as I could, this code makes use of the #11685

import quoted.*

import scala.annotation.experimental

object macroMinimized2:
  inline def derive[Alg[_[_]], G[_]] = ${ instanceK[Alg, G] }

  // create instance of Alg[F] that has a method foo(i: F[Int]): Unit
  @experimental def instanceK[Alg[_[_]]: Type, G[_]: Type](using Quotes): Expr[Alg[G]] =
    import quotes.reflect.*

    val name = "$anon()"
    val parents = List(TypeTree.of[Object], TypeTree.of[Alg[G]])
    // uncomment to make it work on the second run
    // val parents = List(TypeTree.of[Object], TypeTree.of[Alg[scala.util.Try]]) 
    val method = definedMethodsInType[Alg].head
    // this should be used, since it feels like there is a problem in change owner
    // method arg types are aligned with the first macro call 
    def decls(cls: Symbol): List[Symbol] =
      method.tree.changeOwner(cls) match 
        case DefDef(name, clauses, typedTree, _) =>
          val tpeRepr       = TypeRepr.of(using typedTree.tpe.asType)
          val (nms, tpes) = clauses.map(_.params.collect { case v: ValDef => (v.name, v.tpt.tpe) }.unzip).head
          println("---------")
          println(tpes)
          println("---------")
          // tpes is always the same across multiple derive calls
          val methodType = MethodType(nms)(_ => tpes, _ => tpeRepr)

          Symbol.newMethod(
            cls,
            name,
            methodType,
            flags = Flags.EmptyFlags /*TODO: method.flags */,
            privateWithin = method.privateWithin.fold(Symbol.noSymbol)(_.typeSymbol)
          ) :: Nil
        case _ =>
          report.errorAndAbort(s"Cannot detect type of method: ${method.name}")

    // uncomment this one to make it work
    // def decls(cls: Symbol): List[Symbol] =
    //   List(Symbol.newMethod(cls, "foo", MethodType(List("id"))(_ => List(TypeRepr.of[G[Int]]), _ => TypeRepr.of[Unit])))

    val cls = Symbol.newClass(Symbol.spliceOwner, name, parents = parents.map(_.tpe), decls, selfType = None)
    val fooSym = cls.declaredMethod("foo").head

    val fooDef = DefDef(fooSym, argss => Some('{println(s"Calling foo")}.asTerm))
    val clsDef = ClassDef(cls, parents, body = List(fooDef))
    val newCls = Typed(Apply(Select(New(TypeIdent(cls)), cls.primaryConstructor), Nil), TypeTree.of[Alg[G]])

    val res = Block(List(clsDef), newCls).asExprOf[Alg[G]]

    println("---------")
    println(res.show)
    println("---------")

    res

  // function to get methods defined for the type
  def definedMethodsInType[Alg[_[_]]: Type](using Quotes): List[quotes.reflect.Symbol] =
    import quotes.reflect.*

    val cls = TypeRepr.of[Alg].typeSymbol

    for {
      member <- cls.methodMembers
      // is abstract method, not implemented
      if member.flags.is(Flags.Deferred)

      // TODO: is that public?
      // TODO? if member.privateWithin
      if !member.flags.is(Flags.Private)
      if !member.flags.is(Flags.Protected)
      if !member.flags.is(Flags.PrivateLocal)

      if !member.isClassConstructor
      if !member.flags.is(Flags.Synthetic)
    } yield member

Output

  1. if it's a single call per code base macro works as expected
  2. If there is more than a single call (macroMinimized2.derive[Foo, Option], macroMinimized2.derive[Foo, Try], etc) all the followup calls generate traits with incorrect method signatures:
// Call 1: macroMinimized2.derive[Foo, Option]
// Generated method: def foo(id: scala.Option[scala.Int])
---------
List(AppliedType(TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class <root>)),object scala),class Option),List(TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class <root>)),object scala),class Int))))
---------
{
  class $anon() extends java.lang.Object with MinimizedSpec.this.Foo[[A >: scala.Nothing <: scala.Any] => scala.Option[A]] {
    def foo(id: scala.Option[scala.Int]): scala.Unit = scala.Predef.println(_root_.scala.StringContext.apply("Calling foo").s())
  }

  (new $anon()(): MinimizedSpec.this.Foo[[A >: scala.Nothing <: scala.Any] => scala.Option[A]])
}
---------

// Call 2: macroMinimized2.derive[Foo, Try]
// Generated method: def foo(id: scala.Option[scala.Int])
---------
List(AppliedType(TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class <root>)),object scala),class Option),List(TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class <root>)),object scala),class Int))))
---------
{
  class $anon() extends java.lang.Object with MinimizedSpec.this.Foo[[T >: scala.Nothing <: scala.Any] => scala.util.Try[T]] {
    def foo(id: scala.Option[scala.Int]): scala.Unit = scala.Predef.println(_root_.scala.StringContext.apply("Calling foo").s())
  }

  (new $anon()(): MinimizedSpec.this.Foo[[T >: scala.Nothing <: scala.Any] => scala.util.Try[T]])
}
---------

Expectation

Should not fail and generate the correct trait body when called multiple times.

Metadata

Metadata

Assignees

No one assigned

    Labels

    itype:bugstat:needs triageEvery issue needs to have an "area" and "itype" label

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions