Skip to content

Possible bug in tasty reflection regarding matching of trees #12253

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

Closed
wookievx opened this issue Apr 28, 2021 · 6 comments · Fixed by #12254 or #12255
Closed

Possible bug in tasty reflection regarding matching of trees #12253

wookievx opened this issue Apr 28, 2021 · 6 comments · Fixed by #12254 or #12255
Assignees
Milestone

Comments

@wookievx
Copy link

wookievx commented Apr 28, 2021

Compiler version

3.0.0-RC3

Minimized example

I am attempting to extract name of the selected field from lambda expression (to create lens-like functionality). I am attempting to remove layers of inlined because of the passing of the lambda as an inline parameter multiple times

//macros definition
import scala.quoted.{given, *}
import deriving.*,  compiletime.*

object MacroUtils:
  transparent inline def extracNameFromSelector[To, T](inline code: To => T) = ${extractNameFromSelectorImpl('code)}

  def extractNameFromSelectorImpl[To: Type, T: Type](code: Expr[To => T])(using Quotes): Expr[String] = 
    import quotes.reflect.*
    val extractors = new Extractors
    code.asTerm match
     case extractors.InlinedLambda(_, Select(_, name)) => Expr(name)
     case t => report.throwError(s"Illegal argument to extractor: ${code.show}, in tasty: $t")

  class Extractors(using val quotes: Quotes):
    //attempt to strip away consecutive inlines in AST and extract only final lambda
    import quotes.reflect.*

    object InlinedLambda:
      def unapply(arg: Term): Option[(List[ValDef], Term)] = 
        arg match
          case Inlined(_, _, Lambda(vals, term)) => Some((vals, term))
          case Inlined(_, _, nested) => InlinedLambda.unapply(nested)
          case t => None
    end InlinedLambda

  end Extractors
end MacroUtils

object Usage:
  case class Bar(x: Int, y: String, z: (Double, Double))
  MacroUtils.extractNameFromSelector[Bar, String](_.y + "abc")
end Usage

Full example can be found here:

https://github.com/wookievx/chimney/blob/chimney3/chimney3/src/test/scala/io/scalaland/chimney/TransformerDslSpec.scala#L69

Output

When attempting to compile Usage I get the following compilation exception:

  • "java.lang.ClassCastException: dotty.tools.dotc.ast.Trees$Apply cannot be cast to dotty.tools.dotc.ast.Trees$Select"

When macro is invoked with correct lambda body (for example: _.y), the following error is reported in the macro instead (but name extraction works fine):

  • (at line case extractors.InlinedLambda(, Select(, name)): "the type test for extractors.quotes.reflect.Term cannot be checked at runtime"

I am suspecting that I messed-up imports and I am using wrong extractors (via importing quotes.reflect multiple times or something like that)

Expectation

I am trying to provide nice error message in case passed lambda expression is incorrect (which should be matched correctly given my understanding of quotes.reflect extractors) and also avoid this warning, which hints that for some reason some unsound pattern matching is taking place in macro implementation.

@nicolasstucki
Copy link
Contributor

The waarning and error are correct.

-- Warning: Foo.scala:11:10 ----------------------------------------------------
11 |     case extractors.InlinedLambda(_, Select(_, name)) => Expr(name)
   |          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |the type test for extractors.quotes.reflect.Term cannot be checked at runtime

Here we see that the extractor is returning a extractors.quotes.reflect.Term with is not the same as a quotes.reflect.Term as the first is kept in a field. The solution is simple, do not keep the contextual parameter in a field. In general, it is never a good idea to keep a contextual parameter in a field.

To fix the code it just implement the extractor directly in an object:

  def extractNameFromSelectorImpl[To: Type, T: Type](code: Expr[To => T])(using Quotes): Expr[String] =
    import quotes.reflect.*
    code.asTerm match
      case InlinedLambda(_, Select(_, name)) => Expr(name)
      case t => report.throwError(s"Illegal argument to extractor: ${code.show}, in tasty: $t")

  object InlinedLambda:
    def unapply(using Quotes)(arg: quotes.reflect.Term): Option[(List[quotes.reflect.ValDef], quotes.reflect.Term)] =
      import quotes.reflect.*
      arg match
        case Inlined(_, _, Lambda(vals, term)) => Some((vals, term))
        case Inlined(_, _, nested) => InlinedLambda.unapply(nested)
        case t => None
  end InlinedLambda

@nicolasstucki nicolasstucki self-assigned this Apr 28, 2021
@wookievx
Copy link
Author

Thank you, that solves my issue, hopefully someone else will learn from my mistakes.

@nicolasstucki
Copy link
Contributor

Also, this can be simplified

- import scala.quoted.{given, *}
+ import scala.quoted.*

@nicolasstucki
Copy link
Contributor

I have seen this pattern several times and plan to include examples the macro tutorial.

nicolasstucki added a commit to dotty-staging/dotty that referenced this issue Apr 28, 2021
nicolasstucki added a commit to dotty-staging/dotty that referenced this issue Apr 28, 2021
This will help with the siuation found in scala#12253
@nicolasstucki nicolasstucki linked a pull request Apr 28, 2021 that will close this issue
@nicolasstucki
Copy link
Contributor

@wookievx. These additions to the macro tutorial may be helpful o avoid this kind of errors in the future. scala/docs.scala-lang#1999

@wookievx
Copy link
Author

I hope that this will be as helpful for others as it was for me. Great addition.

@Kordyjan Kordyjan added this to the 3.0.1 milestone Aug 2, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
3 participants