Search code examples
scalascala-macrosscala-3tasty

How to extract all methods in scala 3 using a tasty inspector?


I am trying to use a tasty inspector to convert method params to case classes but I get a classcast exception at runtime.

My code:

import dotty.tools.dotc.ast.Trees.{PackageDef, Template}

import scala.quoted.*
import scala.tasty.inspector.*

class MyInspector extends Inspector:
  def inspect(using Quotes)(tastys: List[Tasty[quotes.type]]): Unit =
    for tasty <- tastys do
      import tasty.quotes.reflect.*
      tasty.ast match {
        case PackageDef(pid, stats) =>
          stats.collect { case TypeDef(typeName, Template(constr, parentsOrDerived, self, preBody: List[_])) =>
            preBody.collect { case DefDef(name, paramss: List[List[_]] @unchecked, tpt, preRhs) =>
              val params = paramss.flatten.map { case ValDef(name, tpt, preRhs) =>
                s"$name : ${tpt.show}"
              }
              println(s"""
                   |case class ${typeName}_${name}_ccIn(${params.mkString(", ")})
                   |""".stripMargin)
              println("------------------------")
            }
          }
      }

@main def tryit() =
  val tastyFiles = List("../example-commands/classpath-1/target/scala-3.2.1/classes/cp1/Cp1Exports.tasty")
  TastyInspector.inspectTastyFiles(tastyFiles)(new MyInspector)

I run this against this class (after I compile it and the compiler creates a .tasty file):

package cp1

import java.time.LocalDate

trait Cp1Exports:
  def add(a: Int, b: Int): Int
  def subtract(a: Int, b: Int): Int
  def friends(p: Person, from: LocalDate): Seq[Person]

case class Person(id: Int, name: String)

But I get this exception:

Exception in thread "main" java.lang.ClassCastException: class dotty.tools.dotc.ast.Trees$Import cannot be cast to class dotty.tools.dotc.ast.Trees$TypeDef (dotty.tools.dotc.ast.Trees$Import and dotty.tools.dotc.ast.Trees$TypeDef are in unnamed module of loader 'app')
    at scala.quoted.runtime.impl.QuotesImpl$reflect$TypeDef$.unapply(QuotesImpl.scala:339)
    at console.macros.MyInspector$$anon$1.applyOrElse(MyInspector.scala:15)

The line causing the issue is this:

          stats.collect { case TypeDef(typeName, Template(constr, parentsOrDerived, self, preBody: List[_])) =>

But it shouldn't because this is a collect. The error is caused because there is an import in Cp1Exports. If I remove the import, it works.

Also any advice to simplify the code would be appreciated.

I am using scala 3.2.1 (incl scala-compiler with that version)

EDIT:

Ok after following the advice from below, I ended up with this code which works (but seems rather complicated):

import dotty.tools.dotc.ast.Trees.*

import scala.quoted.*
import scala.tasty.inspector.*

class MyInspector extends Inspector:
  def inspect(using Quotes)(tastys: List[Tasty[quotes.type]]): Unit =
    for tasty <- tastys do
      given dotty.tools.dotc.core.Contexts.Context = scala.quoted.quotes.asInstanceOf[scala.quoted.runtime.impl.QuotesImpl].ctx
      tasty.ast match {
        case PackageDef(pid, stats) =>
          stats.collect { case TypeDef(typeName, Template(constr, parentsOrDerived, self, preBody: List[_])) =>
            preBody.collect { case DefDef(name, paramss: List[List[_]] @unchecked, tpt, preRhs) =>
              val params = paramss.flatten.map { case ValDef(name, tpt, preRhs) =>
                s"$name : ${tpt.show}"
              }
              println(s"""
                   |case class ${typeName}_${name}_ccIn(${params.mkString(", ")})
                   |""".stripMargin)
              println("------------------------")
            }
          }
      }

@main def tryit() =
  val tastyFiles = List("../example-commands/classpath-1/target/scala-3.2.1/classes/cp1/Cp1Exports.tasty")
  TastyInspector.inspectTastyFiles(tastyFiles)(new MyInspector)

Thanks


Solution

  • I noticed that

    stats.collect { case dotty.tools.dotc.ast.Trees.TypeDef(_, _) => } 
    

    doesn't throw while

    stats.collect { TypeDef(_, _) => } 
    

    aka

    stats.collect { tasty.quotes.reflect.TypeDef(_, _) => }
    

    does.

    The difference seems to be that dotty.tools.dotc.ast.Trees.TypeDef is a case class while tasty.quotes.reflect.TypeDef is an abstract type.

    The reason seems to be type erasure

    Understanding why inlining function fixes runtime crash when collecting

    Simpler reproduction:

    import scala.reflect.TypeTest
    
    trait X {
      type A
    
      type B <: A
      trait BModule {
        def unapply(b: B): Option[Int]
      }
      val B: BModule
      given BTypeTest: TypeTest[A, B]
    
      type C <: A
      trait CModule {
        def unapply(c: C): Option[String]
      }
      val C: CModule
      given CTypeTest: TypeTest[A, C]
    }
    
    object XImpl extends X {
      sealed trait A
    
      case class B(i: Int) extends A
      object B extends BModule {
        def unapply(b: B): Option[Int] = Some(b.i)
      }
      object BTypeTest extends TypeTest[A, B] {
        override def unapply(x: A): Option[x.type & B] = x match {
          case x: (B & x.type) => Some(x)
          case _ => None
        }
      }
    
      case class C(s: String) extends A
      object C extends CModule {
        def unapply(c: C): Option[String] = Some(c.s)
      }
      object CTypeTest extends TypeTest[A, C] {
        override def unapply(x: A): Option[x.type & C] = x match {
          case x: (C & x.type) => Some(x)
          case _ => None
        }
      }
    }
    
    def foo()(using x: X) = {
      import x.*
    
      List(XImpl.B(1), XImpl.C("a")).collect { case C(s) => println(s) }
    }
    
    given X = XImpl
    
    foo() // ClassCastException: XImpl$B cannot be cast to XImpl$C
    

    Here X, A, B, C are similar to Quotes, Tree, ValDef, TypeDef.