Skip to content

Misbehavior of macro-generated match when matching case object with lowercase name #20350

Open
@MateuszKubuszok

Description

@MateuszKubuszok

Compiler version

3.3.3

Minimized code

repro.scala

//> using scala 3.3.3

import scala.quoted.*

object Macros {

  def matchOnImpl[A: Type](a: Expr[A])(using quotes: Quotes): Expr[Unit] = {
    import quotes.*, quotes.reflect.*

    // workaround to contain @experimental from polluting the whole codebase
    object FreshTerm {
      private val impl = quotes.reflect.Symbol.getClass.getMethod("freshName", classOf[String])

      def generate(prefix: String): String = impl.invoke(quotes.reflect.Symbol, prefix).asInstanceOf[String]
    }

    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 cases = extractSealedSubtypes[A].map { tpe =>
        val sym = TypeRepr.of(using tpe).typeSymbol
        val bindName = Symbol.newVal(Symbol.spliceOwner, FreshTerm.generate(sym.name.toLowerCase), TypeRepr.of[A],Flags.EmptyFlags, Symbol.noSymbol)
        val body = '{ println(${ Expr(sym.name) }) }.asTerm

        if sym.flags.is(Flags.Enum | Flags.JavaStatic) then
          CaseDef(Bind(bindName, Ident(sym.termRef)), None, body)
        else if sym.flags.is(Flags.Module) then
          CaseDef(
            Bind(bindName, Ident(sym.companionModule.termRef)),
            None,
            body
          )
        else
          CaseDef(Bind(bindName, Typed(Wildcard(), TypeTree.of(using tpe))), None, body)
      }
      Match(a.asTerm, cases).asExprOf[Unit]
    } else '{ () }
  }

  inline def matchOn[A](a: A): Unit = ${ matchOnImpl[A]('{ a }) }
}

repro.test.scala

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

package test

sealed trait Upper
object Upper {
  case object A extends Upper
  case object B extends Upper
  case object C extends Upper
}

sealed trait lower
object lower {
  case object a extends lower
  case object b extends lower
  case object c extends lower
}

class Test extends munit.FunSuite {

  test("should print its own name") {
    Macros.matchOn[Upper](Upper.A)
    Macros.matchOn[Upper](Upper.B)
    Macros.matchOn[Upper](Upper.C)

    Macros.matchOn[lower](lower.a)
    Macros.matchOn[lower](lower.b)
    Macros.matchOn[lower](lower.c)
  }
}
scala-cli test .

Output

A$
B$
C$
a$
a$
a$

Expectation

A$
B$
C$
a$
b$
c$

When case object with lowercased names is used match seem to fall through on the first case. For the same macro code the behavior is correct if the name of case object starts with an upper case.

I haven't observed such issue with enums.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions