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

TypeRepr of constructor parameters unreliable - dependent on compilation order #19795

Closed
OndrejSpanel opened this issue Feb 27, 2024 · 4 comments
Assignees
Labels
area:metaprogramming:reflection Issues related to the quotes reflection API itype:bug

Comments

@OndrejSpanel
Copy link
Member

The TypeRepr of class constructor parameters is sometimes wrong. Instead of an AppliedType sometimes a plain TypeRef is returned, missing type parameters.

Compiler version

3.0.0, 3.3.2, 3.4.1-RC1

Minimized code

https://github.com/OndrejSpanel/surface-sandbox

The repo contains two reproduction methods: default branch contains a minimized repro, branch surface-repro contains repro using aiframe-surface library, which is how the issue was originaly found.

  println(Surface.func[TypeUtils.Wrap])
import scala.quoted.*

object Surface:

  def funcImpl[A](using tpe: Type[A], quotes: Quotes): Expr[String] = {
    import quotes.*
    import quotes.reflect.*

    val t = TypeRepr.of(using tpe)
    println(s"func ${t.show}")

    val paramStrings = t.typeSymbol.primaryConstructor.paramSymss.flatten
      .map(s => s.tree.show)
    val output =  paramStrings.mkString(",")
    println(s"params $output")
    Expr(output)
  }


  inline def func[A]: String = ${ funcImpl[A] }
import scala.quoted.*

object TypeUtils {
  trait MyOption[T]

  class Wrap(val option: MyOption[Int])

  def macroImpl(using Quotes): Expr[Unit] = '{ }

  inline def myMacro(): Unit = ${ macroImpl }
}

Output

val localOpt: TypeUtils.MyOption[scala.Int]
val option: TypeUtils.MyOption

Expectation

Output for option should contain correct parameter type including type parameters. same way as localOpt does.

Note

The reproduction depends on compilation order. If you uncomment the line containing TypeUtils.myMacro(), the issue disappears and the output is as expected:

val localOpt: TypeUtils.MyOption[scala.Int]
val option: TypeUtils.MyOption[scala.Int]
@OndrejSpanel OndrejSpanel added itype:bug stat:needs triage Every issue needs to have an "area" and "itype" label labels Feb 27, 2024
@nicolasstucki nicolasstucki added area:metaprogramming:reflection Issues related to the quotes reflection API and removed stat:needs triage Every issue needs to have an "area" and "itype" label labels Feb 27, 2024
@jchyb
Copy link
Contributor

jchyb commented Mar 13, 2024

Symbol.tree is not recommended to use in macros, as it not guaranteed to return the tree, since the compiler itself may not have access to it at that point. Looking at the minimisation, you can achieve the same effect (get the constructor parameter types) by using:

val paramStrings = t.typeSymbol.primaryConstructor.paramSymss.flatten.map(s => t.memberType(s))

With that said, even when passing "-Yretain-trees" compilation option, which should help, I see that the type parameter can still be missing, so this is worth looking into.

Minimisation for scalac:
macro.scala

import scala.quoted.*

object Surface:

  def funcImpl[A](using tpe: Type[A], quotes: Quotes): Expr[String] = {
    import quotes.reflect.*

    val t = TypeRepr.of(using tpe)

    val paramStrings = t.typeSymbol.primaryConstructor.paramSymss.flatten
      .map(s => s.tree.show)
    val output =  paramStrings.mkString(",")
    println(s"params $output")
    Expr(output)
  }

  inline def func[A]: String = ${ funcImpl[A] }

main.scala:

object Main {
  println(Surface.func[TypeUtils.Wrap])
}

TypeUtils.scala

object TypeUtils {
  trait MyOption[T]
  class Wrap(val option: MyOption[Int])
}

Running:
scalac -d . macro.scala main.scala TypeUtils.scala
will print:

params val option: TypeUtils.MyOption

After which, running something like (with paths to stdlibs adjusted, and the previous output added onto the clsspath like a build tool would do):
scalac -cp /Users/jchyb/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/org/scala-lang/scala-library/2.13.12/scala-library-2.13.12.jar:/Users/jchyb/Documents/workspace/dotty/library/../out/bootstrap/scala3-library-bootstrapped/scala-3.4.2-RC1-bin-SNAPSHOT-nonbootstrapped/scala3-library_3-3.4.2-RC1-bin-SNAPSHOT.jar:. -d . main.scala
will print the correct type:

params val option: TypeUtils.MyOption[scala.Int]

@OndrejSpanel
Copy link
Member Author

OndrejSpanel commented Mar 20, 2024

you can achieve the same effect (get the constructor parameter types) by using:

val paramStrings = t.typeSymbol.primaryConstructor.paramSymss.flatten.map(s => t.memberType(s))

Thanks for the advice. This works very well for my use case. I will submit a PR to airframe implementing this.

xerial pushed a commit to wvlet/airframe that referenced this issue Mar 22, 2024
… types (#3466)

Do not use `.tree` to obtain parameter types, use `typeMember` instead.
The code is simpler and more reliable, avoiding issue
scala/scala3#19795 (#3417)
@jchyb
Copy link
Contributor

jchyb commented Mar 26, 2024

After investigating for a bit I am afraid that in general this is not something we can/should fix. The issue shows itself when compiling 2 times. First when compiling all files simultenaously, and second, when recompiling only the macro and main methods, using previous artifacts in the way of incremental compilation.
The first compilation shows a TypeTree without a type parameter, and the second will show one with type parameter.

To understand why this is the case we need to follow how the macro is being expanded by the compiler and what the tree method does.

The symbol.tree method return the program tree that is being associated with the symbol at the point of the macro expansion. So for example, that tree can be different for transparent inline and non-transparent macros by design, as both happen at the different stages of compilation. This is also affected by the way files are suspended when compiling.
In our first case, the macro code depends on another file (compilation unit) TypeUtils.scala, so that file is fully compiled before the macro is being expanded. This causes the the program tree there to go through the TypeErasure phase, which removes type parameters, among other things. So by the time we inspect the tree there, the type parameter no longer exists.
In the second case, when expansing the macros both the symbol and tree are read from the TASTy file produced in a previous compilation, which includes those type parameters.

In summary:
Both trees are correct, the first is generated after TypeErasure due to this being the first compilation (so it makes sense that it does not have a type parameter), and the second after unpickling TASTy (where type parameters are kept).

@jchyb jchyb closed this as completed Mar 26, 2024
@OndrejSpanel
Copy link
Member Author

Thanks for investigation. I have created PR for the library meanwhile using your suggested code and I will try to be extra careful anytime I see .tree somewhere.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area:metaprogramming:reflection Issues related to the quotes reflection API itype:bug
Projects
None yet
Development

No branches or pull requests

3 participants