Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
pomadchin opened this issue Aug 26, 2022 · 1 comment
Labels
itype:bug stat:needs triage Every issue needs to have an "area" and "itype" label

Comments

@pomadchin
Copy link

pomadchin commented Aug 26, 2022

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.

@pomadchin pomadchin added itype:bug stat:needs triage Every issue needs to have an "area" and "itype" label labels Aug 26, 2022
@pomadchin pomadchin changed the title Unexpected macro argument type substitution on the method owner change Unexpected macro argument type substitution on the method owner change in the Symbol.newClass call Aug 26, 2022
@pomadchin
Copy link
Author

pomadchin commented Aug 26, 2022

Actually @dos65 suggested an alternative solution (https://github.com/pomadchin/tagless-derivation/pull/8/files#diff-b38fb19564175dfc117cad2673e2f062e3cbb6c7b2971aa642a9ff65b13ba905R16-R26) so we don't really need it, and the same (the desired behavior) is achievable with the much less code amount (also working and being consistent in its behavior):

val algFApplied = TypeRepr.of[Alg[F]]
val asSeenApplied = algFApplied.memberType(method)
Symbol.newMethod(
  clz,
  method.name,
  asSeenApplied,
  ...
)

May be this code is not an appropriate use of this functionality.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
itype:bug stat:needs triage Every issue needs to have an "area" and "itype" label
Projects
None yet
Development

No branches or pull requests

1 participant