Skip to content

Inconsistent handling of match types in pattern matching #17719

@felher

Description

@felher

Hey all,

while playing around with extractors and compile time ops, I noticed something which might be a compiler bug. When one has an unapply: Option[(String, String)], one normally only needs one layer of parentheses in a pattern match, i.e. case Unapply(a, b) => .... But when I derived the (String, String) type via match types and used two different approaches which to me seemed equivalent, one of them required an extra level of parentheses, i.e. case Unapply((a, b)) => ....

If I had to guess, one of those two methods is not reduced to a tuple at the time the patmat code checks whether it is a tuple, but of course I have no knowledge of the compiler internals. The code is below, you can find a scastie here: https://scastie.scala-lang.org/HfRYp0utTNKjRQjptpVrFQ

Compiler version

3.1.2

Minimized code

import scala.compiletime.ops.int.*
import scala.compiletime.*

// make the string tuple of length N type
type TupleN[X] = X match
  case 0 => EmptyTuple
  case 2 => (String, String)

// same, but written differently
type TupleNRec[X] = X match
  case 0 => EmptyTuple
  case _ => Tuple.Concat[TupleNRec[X - 1], Tuple1[String]]

// basic unapply to use in pattern match
trait Unapplyable[A] {
  def unapply(s: String): Option[A]
}

// create a extractor for a string tuple of size N
inline def extractN[N]: Unapplyable[TupleN[N]] =
  val length = constValue[N]
  new Unapplyable[TupleN[N]]:
    def unapply(s: String): Option[TupleN[N]] =
      val split = s.split(";")
      if split.length == length then Some(Tuple.fromArray(split).asInstanceOf[TupleN[N]])
      else None

// the same, but using the recursive type defintion
inline def extractNRec[N]: Unapplyable[TupleNRec[N]] =
  val length = constValue[N]
  new Unapplyable[TupleNRec[N]]:
    def unapply(s: String): Option[TupleNRec[N]] =
      val split = s.split(";")
      if split.length == length then Some(Tuple.fromArray(split).asInstanceOf[TupleNRec[N]])
      else None

// scala seems to think that those are the same types
summon[TupleN[2] =:= TupleNRec[2]]

// the second arm only compiles with ((a, b)), not (a, b)
val test = "a;b" match
  case extractN[2](a, b) => ()
  case extractNRec[2](a, b) => () 

Output

Wrong number of argument patterns for Playground.extractNRec[2.type]; expected: ((String, String))

Expectation

It should compile. Both should use the same number of parentheses.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions