Skip to content

Unable to match a lower-case constructor in quoted pattern #22616

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

Open
Kalin-Rudnicki opened this issue Feb 17, 2025 · 11 comments
Open

Unable to match a lower-case constructor in quoted pattern #22616

Kalin-Rudnicki opened this issue Feb 17, 2025 · 11 comments
Assignees
Labels

Comments

@Kalin-Rudnicki
Copy link

Compiler version

3.6.3

Minimized code

final case class caseName(name: String) extends scala.annotation.Annotation
object caseName {

  given FromExpr[caseName] =
    new FromExpr[caseName] {
      override def unapply(x: Expr[caseName])(using Quotes): Option[caseName] =
        x match {
          case '{ caseName(${ Expr(name) }) }     => caseName(name).some
          // with/without the following line...
          case '{ new caseName(${ Expr(name) }) } => caseName(name).some
          case _                                  => println(x.show); None
        }
    }

}
sealed trait SealedTrait3[+A, +B] derives Show
object SealedTrait3 {
  final case class AB1[+B, +A](a: B, b: A) extends SealedTrait3[B, A]
  final case class AB2[+C, +D](a: C, b: D) extends SealedTrait3[D, C]
  final case class A[+A](a: A) extends SealedTrait3[A, Nothing]
  @caseName("_B_") final case class B[+B](b: B) extends SealedTrait3[Nothing, B]
  case object Neither extends SealedTrait3[Nothing, Nothing]
}
    def optionalAnnotation[Annot: Type]: Option[Expr[Annot]] = {
      val annotTpe = TypeRepr.of[Annot]
      val annotFlags = annotTpe.typeSymbol.flags

      if (annotFlags.is(Flags.Abstract) || annotFlags.is(Flags.Trait))
        report.errorAndAbort(s"Bad annotation type ${annotTpe.show} is abstract")

      this.getAnnotation(annotTpe.typeSymbol) match
        case Some(tree) if tree.tpe <:< annotTpe => tree.asExprOf[Annot].some
        case _                                   => None
    }

    def requiredAnnotation[Annot: Type]: Expr[Annot] =
      optionalAnnotation[Annot].getOrElse(report.errorAndAbort(s"Missing required annotation `${TypeRepr.of[Annot].show}` for `$this`"))

    def optionalAnnotationValue[Annot: {Type, FromExpr}]: Option[Annot] =
      optionalAnnotation[Annot].map { expr =>
        expr.value.getOrElse(report.errorAndAbort(s"Found annotation `${TypeRepr.of[Annot].show}` for `$this`, but are unable to extract Expr.value\n${expr.show}"))
      }

    def requiredAnnotationValue[Annot: {Type, FromExpr}]: Annot = {
      val expr = requiredAnnotation[Annot]
      expr.value.getOrElse(report.errorAndAbort(s"Found annotation `${TypeRepr.of[Annot].show}` for `$this`, but are unable to extract Expr.value\n${expr.show}"))
    }

Output

[error]    |Found annotation `oxygen.meta.example.caseName` for `oxygen.meta.example.SealedTrait3.B[B]`, but are unable to extract Expr.value
[error]    |new oxygen.meta.example.caseName("_B_")

then add a case for that in the FromExpr, and...

[error] 28 |          case '{ new caseName(${ Expr(name) }) } => caseName(name).some
[error]    |                      ^^^^^^^^
[error]    |                      caseName does not have a constructor

Expectation

That either:

  1. case '{ caseName(${ Expr(name) }) } => caseName(name).some matches
  2. case '{ new caseName(${ Expr(name) }) } => caseName(name).some is allowed, and matches

Quite difficult to handle when you see "we found this, but cant match on it", and then when you try to match on the exact expr.show, it says that expr is not valid?

@Kalin-Rudnicki Kalin-Rudnicki added itype:bug stat:needs triage Every issue needs to have an "area" and "itype" label labels Feb 17, 2025
@som-snytt
Copy link
Contributor

I don't know the details, but a recent conversation about annotation includes #22553

@Gedochao Gedochao added area:annotations 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 Feb 17, 2025
@mbovel mbovel self-assigned this Feb 17, 2025
@mbovel
Copy link
Member

mbovel commented Feb 17, 2025

I can have a look; @Gedochao can we assign this to me for this cycle?

@Kalin-Rudnicki
Copy link
Author

Thanks @mbovel!
Also, reading the description for scala.annotation.ConstantAnnotation, it really seems like for any A <: ConstantAnnotation, the compiler should be providing a given FromExpr[A]. Im curious your thoughts on this, and whether that might be doable here as well?

@Kalin-Rudnicki
Copy link
Author

I guess, generally it would be nice if the compiler provided a built-in mechanism for deriving FromExpr[A] for any product/sum type, but I believe it would be correct to say that a correctly written auto-derive for a ConstantAnnotation should always return Some[A]?

@mbovel
Copy link
Member

mbovel commented Apr 23, 2025

The problem does not seem linked to annotations or classes extending annotations. It seems to be linked to classes with lower-case names.

Compiles successfully:

import scala.quoted.{Expr, Quotes}

class Foo(name: String) extends scala.annotation.StaticAnnotation

object Macro:
  inline def myMacro3(): Unit =
    ${ myMacroImpl3('{new Foo("hello")}) }

  def myMacroImpl3(x: Expr[Foo])(using q: Quotes): Expr[Unit] =
    x match
      case '{new Foo(${Expr(name)})} =>
        println(name)
      case _ =>
        println("not a Foo")

    '{()}
$ scala compile --server=false -S 3.6.3 22616
hello

Errors out:

import scala.quoted.{FromExpr, Expr, Quotes}

class foo(name: String)

object Macro:
  inline def myMacro3(): Unit =
    ${ myMacroImpl3('{new foo("hello")}) }

  def myMacroImpl3(x: Expr[foo])(using q: Quotes): Expr[Unit] =
    x match
      case '{new foo(${Expr(name)})} =>
        println(name)
      case _ =>
        println("not a foo")

    '{()}
$ scala compile --server=false -S 3.6.3 22616
-- Error: /localhome/bovel/scala3/22616/defs.scala:11:17 -------------------------------------------------------------------------------------------------------------------------
11 |      case '{new foo(${Expr(name)})} =>
   |                 ^^^
   |                 foo does not have a constructor
-- Error: /localhome/bovel/scala3/22616/defs.scala:11:27 -------------------------------------------------------------------------------------------------------------------------
11 |      case '{new foo(${Expr(name)})} =>
   |                       ^^^^^^^^^^
   |                       Type must be fully defined.
   |                       Consider annotating the splice using a type ascription:
   |                         (${Expr(name)}: XYZ).
-- [E006] Not Found Error: /localhome/bovel/scala3/22616/defs.scala:12:16 --------------------------------------------------------------------------------------------------------
12 |        println(name)
   |                ^^^^
   |                Not found: name - did you mean Some?
   |
   | longer explanation available when compiling with `-explain`
3 errors found
Compilation failed
$ scala compile -Ydebug-error --server=false -S 3.6.3 22616
-- Error: /localhome/bovel/scala3/22616/defs.scala:11:17 -------------------------------------------------------------------------------------------------------------------------
11 |      case '{new foo(${Expr(name)})} =>
   |                 ^^^
   |                 foo does not have a constructor
java.lang.Exception: Stack trace
        at java.base/java.lang.Thread.dumpStack(Thread.java:1389)
        at dotty.tools.dotc.report$.error(report.scala:70)
        at dotty.tools.dotc.typer.ErrorReporting$.errorType(ErrorReporting.scala:32)
        at dotty.tools.dotc.typer.TypeAssigner.notAMemberErrorType(TypeAssigner.scala:186)
        at dotty.tools.dotc.typer.TypeAssigner.notAMemberErrorType$(TypeAssigner.scala:16)
        at dotty.tools.dotc.typer.Typer.notAMemberErrorType(Typer.scala:148)
        at dotty.tools.dotc.typer.Typer.reportAnError$1(Typer.scala:897)
        at dotty.tools.dotc.typer.Typer.typedSelectWithAdapt(Typer.scala:911)
        at dotty.tools.dotc.typer.Typer.typeSelectOnTerm$1(Typer.scala:996)
        at dotty.tools.dotc.typer.Typer.typedSelect(Typer.scala:1034)
        at dotty.tools.dotc.typer.Typer.typedNamed$1(Typer.scala:3475)
        at dotty.tools.dotc.typer.Typer.typedUnadapted(Typer.scala:3584)
        at dotty.tools.dotc.typer.Typer.typed(Typer.scala:3662)
        at dotty.tools.dotc.typer.Typer.typed(Typer.scala:3666)
        at dotty.tools.dotc.typer.Typer.typedExpr(Typer.scala:3777)
        at dotty.tools.dotc.typer.Applications.realApply$1(Applications.scala:1051)
        at dotty.tools.dotc.typer.Applications.typedApply(Applications.scala:1244)
        at dotty.tools.dotc.typer.Applications.typedApply$(Applications.scala:434)
        at dotty.tools.dotc.typer.Typer.typedApply(Typer.scala:148)
        at dotty.tools.dotc.typer.Typer.typedUnnamed$1(Typer.scala:3500)
        at dotty.tools.dotc.typer.Typer.typedUnadapted(Typer.scala:3585)
        at dotty.tools.dotc.typer.Typer.typed(Typer.scala:3662)
        at dotty.tools.dotc.typer.Typer.typed(Typer.scala:3666)
        at dotty.tools.dotc.typer.Typer.typedExpr(Typer.scala:3777)
        at dotty.tools.dotc.typer.QuotesAndSplices.typedQuotePattern(QuotesAndSplices.scala:286)
        at dotty.tools.dotc.typer.QuotesAndSplices.typedQuote(QuotesAndSplices.scala:56)
        at dotty.tools.dotc.typer.QuotesAndSplices.typedQuote$(QuotesAndSplices.scala:32)

@mbovel
Copy link
Member

mbovel commented Apr 23, 2025

Adding backticks enables the lower case version to compile as expected:

  def myMacroImpl3(x: Expr[foo])(using q: Quotes): Expr[Unit] =
    x match
      case '{new `foo`($name)} =>
        println(name)
      case _ =>
        println("not a foo")

@mbovel mbovel changed the title Unable to create unapply for annotation Unable to match a lower-case constructor in quoted pattern Apr 23, 2025
@mbovel
Copy link
Member

mbovel commented Apr 23, 2025

I am not sure if the current behavior is expected or is a bug; I can't find a definitive answer in the documentation. In general, what do lower case identifiers in term positions in quoted patterns mean?

I see I can match case '{true} or case '{bar($name)}. So maybe I should also be able to match new foo($name)?

Somehow related: this page says that lower case identifiers in type positions are binders: “Just as in a normal pattern, the type variables are written using lower case names.”

cc @hamzaremmal @jchyb @nicolasstucki

@mbovel
Copy link
Member

mbovel commented Apr 23, 2025

Further thinking about it, I think this is a bug, and that the culprit is this condition in typedIdent:

if untpd.isVarPattern(tree) && name.isTypeName then

The condition is true when typing foo in new foo, while it probably shouldn't.

@jchyb
Copy link
Contributor

jchyb commented Apr 23, 2025

I agree it looks like a bug (especially with that error message). I also believe the type positions part in lower case identifiers in type positions are binders only relates to Typed() and TypeApply() trees (but I can't say that for sure, as I have not really delved to that part of the code yet).

@mbovel
Copy link
Member

mbovel commented Apr 23, 2025

I also believe the type positions part in lower case identifiers in type positions are binders only relates to Typed() and TypeApply() trees

That would make sense to me. Unfortunately, I don't think we can easily know if we are in one of these nodes when typing the nested identifier. At least, mode and prototype do not help:

ctx.mode: Mode(Type,ImplicitsEnabled,QuotedExprPattern)
pt: WildcardType(NoType)

@mbovel
Copy link
Member

mbovel commented Apr 28, 2025

I also believe the type positions part in lower case identifiers in type positions are binders only relates to Typed() and TypeApply() trees (but I can't say that for sure, as I have not really delved to that part of the code yet).

It might actually only be inside Typed trees, given what I observe in practice, and this sentence from the documentation

The pattern $x: t will match an expression of any type and t will be bound to the type of the pattern.

And that even seems to be too general; for example, this doesn't compile:

import scala.quoted.{FromExpr, Expr, Quotes}

case class Foo[T](x: String)

object Macro:
  inline def myMacro3(): Unit =
    ${ myMacroImpl3('{Foo[Any]("hello")}) }

  def myMacroImpl3(x: Expr[Foo[Any]])(using Quotes): Expr[Unit] =
    x match
      case '{ Foo($y: t) } =>
        '{Foo[t]($y); ()}
      case _ =>
        println("not a foo")

    '{()}
-- [E007] Type Mismatch Error: test.scala:11:18 --------------------------------
11 |      case '{ Foo($y: t) } =>
   |                  ^^^^^
   |                  Found:    t
   |                  Required: String
   |
   | longer explanation available when compiling with `-explain`
Set()
-- [E006] Not Found Error: test.scala:12:14 ------------------------------------
12 |        '{Foo[t]($y); ()}
   |              ^
   |              Not found: type t
   |
   | longer explanation available when compiling with `-explain`
-- [E006] Not Found Error: test.scala:12:18 ------------------------------------
12 |        '{Foo[t]($y); ()}
   |                  ^
   |                  Not found: y
   |
   | longer explanation available when compiling with `-explain`
3 errors found

But it does if the field x of the class Foo has type T: case class Foo[T](x: T) 🤔

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants