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

Assertion error during macro generation for an enum #20349

Open
MateuszKubuszok opened this issue May 7, 2024 · 3 comments
Open

Assertion error during macro generation for an enum #20349

MateuszKubuszok opened this issue May 7, 2024 · 3 comments
Labels
area:metaprogramming:quotes Issues related to quotes and splices area:metaprogramming:reflection Issues related to the quotes reflection API area:transform itype:question

Comments

@MateuszKubuszok
Copy link

MateuszKubuszok commented May 7, 2024

Compiler version

3.3.3

Minimized code

repro.scala

//> using scala 3.3.3

import scala.quoted.*

object Macros {


  def valuesImpl[A: Type](using quotes: Quotes): Expr[List[A]] = {
    import quotes.*, quotes.reflect.*

    extension (sym: Symbol)
     def isPublic: Boolean = !sym.isNoSymbol &&
          !(sym.flags.is(Flags.Private) || sym.flags.is(Flags.PrivateLocal) || sym.flags.is(Flags.Protected) ||
            sym.privateWithin.isDefined || sym.protectedWithin.isDefined)

    def isSealed[A: Type]: Boolean =
      TypeRepr.of[A].typeSymbol.flags.is(Flags.Sealed)

    def extractSealedSubtypes[A: Type]: List[Type[?]] = {
      def extractRecursively(sym: Symbol): List[Symbol] =
        if sym.flags.is(Flags.Sealed) then sym.children.flatMap(extractRecursively)
        else if sym.flags.is(Flags.Enum) then List(sym.typeRef.typeSymbol)
        else if sym.flags.is(Flags.Module) then List(sym.typeRef.typeSymbol.moduleClass)
        else List(sym)

      extractRecursively(TypeRepr.of[A].typeSymbol).distinct.map(typeSymbol =>
        typeSymbol.typeRef.asType
      )
    }

    if isSealed[A] then {
      val refs = extractSealedSubtypes[A].flatMap { tpe =>
        val sym = TypeRepr.of(using tpe).typeSymbol
        val isCaseVal = sym.isPublic && sym.flags
          .is(Flags.Case | Flags.Enum) && (sym.flags.is(Flags.JavaStatic) || sym.flags.is(Flags.StableRealizable))

        if (isCaseVal) then List(Ref(sym).asExprOf[A])
        else Nil
      }
      Expr.ofList(refs)
    } else '{ Nil }
  }

  inline def values[A]: List[A] = ${ valuesImpl[A] }
}

repro.test.sc

//> using dep org.scalameta::munit:1.0.0-RC1

object domain:
  enum PaymentMethod:
    case PayPal(email: String)
    case Card(digits: Long, name: String)
    case Cash

println(Macros.values[domain.PaymentMethod])
scala-cli test .

Output

Compiling project (Scala 3.3.3, JVM (17))
Error compiling project (Scala 3.3.3, JVM (17))
Error: Unexpected error when compiling eldupa7_d832c8dfee: java.lang.AssertionError: assertion failed: missing outer accessor in class repro$u002Etest$_

Expectation

repro.test.sc should compile successfully (and print something like List(Cash)).

@MateuszKubuszok MateuszKubuszok added itype:bug stat:needs triage Every issue needs to have an "area" and "itype" label labels May 7, 2024
@Gedochao Gedochao added area:transform area:metaprogramming:reflection Issues related to the quotes reflection API area:metaprogramming:quotes Issues related to quotes and splices and removed stat:needs triage Every issue needs to have an "area" and "itype" label labels May 9, 2024
@jchyb
Copy link
Contributor

jchyb commented May 9, 2024

Let's rewrite repro.test.sc into repr.scala kind of like scala-cli would to make things a bit clearer:

class Test {
  object domain {
    enum PaymentMethod:
      case PayPal(email: String)
      case Card(digits: Long, name: String)
      case Cash
  }
  println(Macros.values[domain.PaymentMethod])
}
object Test {
  lazy val script = new Test()
  def main(args: Array[String]): Unit =
    val _ = script.hashCode()
}

Running this with -Xcheck-macros brings us a more clear problem with the macro method than shown in the compiler crash:

-- Error: /Users/jchyb/workspace/scalabug/2/repro1.scala:8:23 ------------------
 8 |  println(Macros.values[domain.PaymentMethod])
   |          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |Malformed tree was found while expanding macro with -Xcheck-macros.
   |               |The tree does not conform to the compiler's tree invariants.
   |               |
   |               |Macro was:
   |               |scala.quoted.runtime.Expr.splice[scala.collection.immutable.List[Test.this.domain.PaymentMethod]](((evidence$1: scala.quoted.Quotes) ?=> Macros.valuesImpl[Test.this.domain.PaymentMethod](scala.quoted.Type.of[Test.this.domain.PaymentMethod](evidence$1), evidence$1)))
   |               |
   |               |The macro returned:
   |               |scala.List.apply[Test.this.domain.PaymentMethod](PaymentMethod.this.Cash)
   |               |
   |               |Error:
   |               |assertion failed: error while typing PaymentMethod.this, value <local Test> is not contained in object PaymentMethod
   |               |
   |stacktrace available when compiling with `-Ydebug`
   |               |
   |----------------------------------------------------------------------------
   |Inline stack trace
   |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
   |This location contains code that was inlined from repro.scala:50
50 |  inline def values[A]: List[A] = ${ valuesImpl[A] }
   |                                  ^^^^^^^^^^^^^^^^^^
    ----------------------------------------------------------------------------

Evidently, the macro generates us code with PaymentMethod.this.Cash, that it expects to be in context of PaymentMethod (which is why we have this in there). However when we inline that code into the println in Test, we do not have direct access to the PaymentMethod, but instead we have to go through domain.PaymentMethod.

The issue in the macro method lies with the use of typeSymbol.typeRef.asType. typeRef returns us a TypeRepr that is usable only in scope of its owner (https://dotty.epfl.ch/api/scala/quoted/Quotes$reflectModule$SymbolMethods.html#typeRef-d26). Replacing that with TypeRepr.of[A].memberType(typeSymbol) fixes the crash.

@MateuszKubuszok
Copy link
Author

I did some experimentation. I found that:

class OuterClass {
  type InnerType
}

would indeed confuse macros - make them throw AssertionError - when typeSymbol.typeRef is used and would be solved by TypeRepr.of[A].memberType(typeSymbol)... but only for Scala 3 enums:

  • sealed trait with case objects and case classes defined in the companion works fine
  • Java enums defined in a separate .java file (obviously) also works fine

However, for case like:

class Snippet { // simluating what .sc file creates

  enum Foo:
    case Bar(a: Int) // with parameter (separate class)
    case Baz // parameterless (val with valueName.type)
}

for TypeRepr.of[Foo].children.map(c => TypeRepr.of[Foo].memberType(c)) would produce List(Bar, Foo) - parameterless values are upcasted which makes them impossible to handle properly. If TypeRepr.of[A].memberType(subtype) is used to Java enums, they stop working as well.

So while use TypeRepr.of[A].memberType(typeSymbol) instead might the correct answer, it's an incomplete answer, it solves AssertionError but removed the ability to handle some cases.

Perhaps we can consider this bug ticket solved, but I believe that some full canonical answer should be posted here, in case someone else has the same issue.

@OndrejSpanel
Copy link
Member

This reminds me of #19825 - it is impossible to get the correct inner type using typeMember method when dealing with path dependent types.

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

No branches or pull requests

4 participants