Skip to content

Pattern matching with named fields #15437

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

Closed
wants to merge 28 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
fdd41ca
Second draft
Jentsch Feb 23, 2022
9f9ee65
Changed encoding of named pattern matching
Jentsch Feb 24, 2022
ed07f12
Resolve prefer user defined names above case class names
Jentsch Feb 25, 2022
cf2b7ba
Small improvments
Jentsch Mar 16, 2022
f5d5b33
Improve error reporting
Jentsch Jun 8, 2022
db69a3a
Remove hack
Jentsch Jun 8, 2022
24f8b7b
Add handling of duplicated names
Jentsch Jun 8, 2022
5b10bf7
Spelling
Jentsch Jun 8, 2022
7cca3e8
Report member that doesn't support named arguments
Jentsch Jun 8, 2022
207e463
Add more error handling
Jentsch Jun 9, 2022
73a2e5a
Add optionless support
Jentsch Jun 9, 2022
b1e19d3
Remove double lookup
Jentsch Jun 9, 2022
25777c1
Use member lookup for Names
Jentsch Jun 9, 2022
bc8265e
Restore error checking
Jentsch Jun 9, 2022
4438b9b
Remove wrong TODO
Jentsch Jun 9, 2022
bbeb263
Fix error message
Jentsch Jun 9, 2022
fce6731
Use playground.scala
Jentsch Jun 9, 2022
07e8a77
Add seed for helper lib example
Jentsch Jun 9, 2022
40fda63
Add accessiblity test
Jentsch Jun 11, 2022
4c0ce97
Fix test
Jentsch Jun 11, 2022
c23d4c5
Test more excoting types
Jentsch Jun 11, 2022
43d2082
Remove regex hack
Jentsch Jun 11, 2022
2c4b58c
Add todo
Jentsch Jun 11, 2022
4312f86
Remove casting
Jentsch Jun 12, 2022
c0ccb6f
Document minor bug
Jentsch Jun 12, 2022
529ba08
Merge remote-tracking branch 'origin/main' into pattern-matching-with…
Jentsch Jul 10, 2022
01c79b1
Deny named arguments in vararg patterns
Jentsch Jul 10, 2022
ccf4648
Merge remote-tracking branch 'origin/main' into pattern-matching-with…
Jentsch Aug 18, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions compiler/src/dotty/tools/dotc/ast/Trees.scala
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,9 @@ object Trees {

// ----------- Tree case classes ------------------------------------

// TODO: This seems to be a bad workaround of my missing understanding (and why is 206 an underscore?)
def underscore(implicit src: SourceFile): Ident[Untyped] = Ident(SimpleName(206, 1))

/** name */
case class Ident[-T >: Untyped] private[ast] (name: Name)(implicit @constructorOnly src: SourceFile)
extends RefTree[T] {
Expand Down
2 changes: 2 additions & 0 deletions compiler/src/dotty/tools/dotc/config/Feature.scala
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ object Feature:

def namedTypeArgsEnabled(using Context) = enabled(namedTypeArguments)

//TODO: Should I add a feature flag for named pattern matching

def genericNumberLiteralsEnabled(using Context) = enabled(genericNumberLiterals)

def scala2ExperimentalMacroEnabled(using Context) = enabled(scala2macros)
Expand Down
2 changes: 2 additions & 0 deletions compiler/src/dotty/tools/dotc/core/StdNames.scala
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,8 @@ object StdNames {

final val Uninstantiated: TypeName = "?$"

val Names: TypeName = "Names"

val JFunctionPrefix: Seq[TypeName] = (0 to 2).map(i => s"scala.runtime.java8.JFunction${i}")
val JProcedure: Seq[TypeName] = (0 to 22).map(i => s"scala.runtime.function.JProcedure${i}")
}
Expand Down
16 changes: 14 additions & 2 deletions compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2829,10 +2829,22 @@ object Parsers {
p = atSpan(startOffset(t), in.offset) { Apply(p, argumentPatterns()) }
p

/** Patterns ::= Pattern [`,' Pattern]
/** Patterns ::= PatternArgument [`,' PatternArgument]
*/
// TODO: Should the parser handle the rule that named patterns can't come after positional patterns?
def patterns(location: Location = Location.InPattern): List[Tree] =
commaSeparated(() => pattern(location))
commaSeparated(() => namedPattern(location))

/** PatternArgument ::= [id `='] Pattern
*/
def namedPattern(location: Location = Location.InPattern): Tree =
// TODO: Figure out the performance impact of this lookahead
if ((in.token == IDENTIFIER || in.token == BACKQUOTED_IDENT) && in.lookahead.token == EQUALS) then
val ident = termIdent()
accept(EQUALS)
NamedArg(ident.name, pattern(location))
else
pattern(location)

def patternsOpt(location: Location = Location.InPattern): List[Tree] =
if (in.token == RPAREN) Nil else patterns(location)
Expand Down
101 changes: 91 additions & 10 deletions compiler/src/dotty/tools/dotc/typer/Applications.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1387,22 +1387,103 @@ trait Applications extends Compatibility {
res.result()
}

var argTypes = unapplyArgs(unapplyApp.tpe, unapplyFn, args, tree.srcPos)
for (argType <- argTypes) assert(!isBounds(argType), unapplyApp.tpe.show)
val bunchedArgs = argTypes match {
val allArgTypes = unapplyArgs(unapplyApp.tpe, unapplyFn, args, tree.srcPos)
var remainingArgTypes = allArgTypes
for (argType <- remainingArgTypes) assert(!isBounds(argType), unapplyApp.tpe.show)
var bunchedArgs = remainingArgTypes match {
case argType :: Nil =>
if (args.lengthCompare(1) > 0 && Feature.autoTuplingEnabled && defn.isTupleNType(argType)) untpd.Tuple(args) :: Nil
else args
case _ => args
}
if (argTypes.length != bunchedArgs.length) {
report.error(UnapplyInvalidNumberOfArguments(qual, argTypes), tree.srcPos)
argTypes = argTypes.take(args.length) ++
List.fill(argTypes.length - args.length)(WildcardType)

val unapplyPatterns = List.newBuilder[Tree]

// here add positional patterns to unapplyPatterns
// TODO: isInstanceOf seems not to be the right tool
while (bunchedArgs != Nil && remainingArgTypes != Nil && !bunchedArgs.head.isInstanceOf[dotty.tools.dotc.ast.Trees.NamedArg[_]]) {
unapplyPatterns += typed(bunchedArgs.head, remainingArgTypes.head)
bunchedArgs = bunchedArgs.tail
remainingArgTypes = remainingArgTypes.tail
}
val unapplyPatterns = bunchedArgs.lazyZip(argTypes) map (typed(_, _))
val result = assignType(cpy.UnApply(tree)(unapplyFn, unapplyImplicits(unapplyApp), unapplyPatterns), ownType)
unapp.println(s"unapply patterns = $unapplyPatterns")

// named pattern
// TODO: Errors report the wrong position if the name is the error
// TODO: Use proper error reporting
// TODO: Maybe the 'reorder' method above can be reused, or be template
if (bunchedArgs != Nil && remainingArgTypes != Nil) {

val names: SingleDenotation =
mt.resType.member(nme.get).info
.orElse(mt.resType)
.member(tpnme.Names)
.accessibleFrom(selType)
// TODO: Is it possible to get something else than a SingleDenotation?
.asSingleDenotation

val positionOfStringNames: Map[String, Int] =
if qual.symbol.name == nme.unapplySeq then
report.error(i"'${qual}' named patterns are not supported for var arg patterns", qual)
Map.empty
else if names.exists then
// TODO: why doesn't names.info.dealias works?
def dealias(typ: Type): Type = typ match
case alias: TypeAlias => alias.alias
case other => other

dealias(names.info)
// TODO: Error handling if Names is malformed
.tupleElementTypes.getOrElse(Nil)
.map { case ConstantType(Constant(name: String)) => name }
.zipWithIndex
.toMap
// TODO: The test should actually be: is unapplyFn generated by the compiler because we have a case class?
else if unapplyFn.tpe.widen.paramInfoss.head.head.typeSymbol.is(CaseClass) then
unapplyFn.tpe.widen.paramInfoss.head.head.fields.map(_.name.toString).zipWithIndex.toMap
else
report.error(i"'${qual}' doesn't support named patterns", qual)
Map.empty

val positionOfName: PartialFunction[Name, Int] =
// TODO: Is is necessary to convert names to strings?
positionOfStringNames.compose(_.show)

val namedArgs = bunchedArgs
.flatMap {
case pattern @ NamedArg(positionOfName(i), _) if i < unapplyPatterns.knownSize =>
report.error(i"${pattern.name} was already used as a positional pattern", pattern)
Seq.empty
case pattern @ NamedArg(positionOfName(_), _) => Seq(pattern)
case pattern @ NamedArg(unknownName, _) =>
if (positionOfStringNames.nonEmpty)
report.error(s"'${unknownName.show}' is unknown", pattern)
Seq.empty
case unnamedArgument =>
report.error("Only named arguments allowed here", unnamedArgument)
Seq.empty
}
.groupMapReduce(arg => positionOfName(arg.name))(identity)(
(first, second) => {
report.error(s"'${second.name}' was already used before", second);
first
})


while (remainingArgTypes != Nil)
val term = namedArgs.getOrElse(unapplyPatterns.knownSize, {
var ignore = underscore
ignore.span = unapplyFn.span
ignore
})
unapplyPatterns += typed(term, remainingArgTypes.head)
remainingArgTypes = remainingArgTypes.tail
} else {
// Check for positional arguments
if (remainingArgTypes != Nil || bunchedArgs != Nil)
report.error(UnapplyInvalidNumberOfArguments(qual, allArgTypes), tree.srcPos)
}

val result = assignType(cpy.UnApply(tree)(unapplyFn, unapplyImplicits(unapplyApp), unapplyPatterns.result), ownType)
if (ownType.stripped eq selType.stripped) || ownType.isError then result
else tryWithTypeTest(Typed(result, TypeTree(ownType)), selType)
case tp =>
Expand Down
18 changes: 16 additions & 2 deletions compiler/test/dotty/tools/dotc/Playground.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,24 @@ import dotty.tools.vulpix._
import org.junit.Test
import org.junit.Ignore

@Ignore class Playground:
class Playground:
import TestConfiguration._
import CompilationTests._

implicit val testGroup: TestGroup = TestGroup("playground")

@Test def example: Unit =
implicit val testGroup: TestGroup = TestGroup("playground")
compileFile("tests/playground/example.scala", defaultOptions).checkCompile()


@Test def pos: Unit =
compileFile("tests/pos/namedPatternMatching.scala", defaultOptions).checkCompile()

@Test def negativeTests: Unit =
compileFile("tests/neg/negNamedPatternMatching.scala", defaultOptions).checkExpectedErrors()
compileFile("tests/neg/bad-unapplies.scala", defaultOptions).checkExpectedErrors()
compileFile("tests/neg/i10757.scala", defaultOptions).checkExpectedErrors()


@Test def executionTest: Unit =
compileFile("tests/run/runNamedPatternMatching.scala", defaultOptions).checkRuns()
3 changes: 2 additions & 1 deletion docs/_docs/reference/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,9 +319,10 @@ SimplePattern1 ::= SimpleRef
| SimplePattern1 ‘.’ id
PatVar ::= varid
| ‘_’
Patterns ::= Pattern {‘,’ Pattern}
Patterns ::= NamedPattern {‘,’ NamedPattern}
ArgumentPatterns ::= ‘(’ [Patterns] ‘)’
| ‘(’ [Patterns ‘,’] PatVar ‘*’ ‘)’
NamedPattern ::= [id ‘=’] Pattern
```

### Type and Value Parameters
Expand Down
42 changes: 42 additions & 0 deletions tests/neg/negNamedPatternMatching.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
-- Error: tests/neg/negNamedPatternMatching.scala:43:20 ----------------------------------------------------------------
43 | case User(names = "Tom", city = city) => null // error
| ^^^^^
| 'names' is unknown
-- Error: tests/neg/negNamedPatternMatching.scala:44:22 ----------------------------------------------------------------
44 | case User(city = _, 10) => null // error
| ^^
| Only named arguments allowed here
-- Error: tests/neg/negNamedPatternMatching.scala:45:18 ----------------------------------------------------------------
45 | case User(age = Age(years = 10)) => null // error
| ^^^
| 'patterns.Age' doesn't support named patterns
-- Error: tests/neg/negNamedPatternMatching.scala:48:11 ----------------------------------------------------------------
48 | name = "Tom 2", // error
| ^^^^^^^
| 'name' was already used before
-- Error: tests/neg/negNamedPatternMatching.scala:49:11 ----------------------------------------------------------------
49 | name = "Tom 3" // error
| ^^^^^^^
| 'name' was already used before
-- Error: tests/neg/negNamedPatternMatching.scala:51:22 ----------------------------------------------------------------
51 | case User(_, name = "Anna") => null // error
| ^^^^^^
| name was already used as a positional pattern
-- Error: tests/neg/negNamedPatternMatching.scala:52:19 ----------------------------------------------------------------
52 | case User(city = City(name = "Berlin")) => null // error
| ^^^^
| 'patterns.City' doesn't support named patterns
-- Error: tests/neg/negNamedPatternMatching.scala:55:17 ----------------------------------------------------------------
55 |val User(names = notRecursive) = user // error // error
| ^^^^^^^^^^^^
| 'names' is unknown
-- [E045] Cyclic Error: tests/neg/negNamedPatternMatching.scala:55:30 --------------------------------------------------
55 |val User(names = notRecursive) = user // error // error
| ^
| Recursive value notRecursive needs type
|
| longer explanation available when compiling with `-explain`
-- Error: tests/neg/negNamedPatternMatching.scala:63:7 -----------------------------------------------------------------
63 | case MySeq(head = 1, tail = 2) => ??? // error
| ^^^^^
| 'patterns.MySeq' doesn't support named patterns
65 changes: 65 additions & 0 deletions tests/neg/negNamedPatternMatching.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package patterns

type Age = Age.Age

object Age:
opaque type Age = Int

def apply(years: Int): Age = years

def unapply(age: Age): Some[Int] =
Some(age)

type City = City.City

object City:
opaque type City = String

class ExCity(city: City) extends Product:
def _1: String = city

private[City] type Names = "name" *: EmptyTuple

// Members declared in scala.Equals
def canEqual(that: Any): Boolean = ???

// Members declared in scala.Product
def productArity: Int = ???
def productElement(n: Int): Any = ???

City("test") match
case City("test") => null
case _ => null

def apply(name: String): City = name

def unapply(age: City): ExCity = ExCity(age)

case class User(name: String, age: Age, city: City)

val user = User(name = "Anna", age = Age(10), city = City("Berlin"))

val annasCity = user match
case User(names = "Tom", city = city) => null // error
case User(city = _, 10) => null // error
case User(age = Age(years = 10)) => null // error
case User(
name = "Tom",
name = "Tom 2", // error
name = "Tom 3" // error
) => null
case User(_, name = "Anna") => null // error
case User(city = City(name = "Berlin")) => null // error

// TODO: Don't show an error about recursive value
val User(names = notRecursive) = user // error // error


object MySeq:
def unapplySeq[A](x: Seq[A]): Some[Seq[A]] & { type Names = ("first", "second") } =
Some(x).asInstanceOf[Some[Seq[A]] & { type Names = ("first", "second") }]

val x = Seq(1, 2) match {
case MySeq(head = 1, tail = 2) => ??? // error
case _ => println("Also nope")
}
35 changes: 35 additions & 0 deletions tests/pos/namedPatternMatching.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package patterns

class Age(val hidden: Int)

object Age:
def apply(years: Int): Age = new Age(years)

// TODO: Describe alternative encoding with tagged tuples
def unapply(age: Age): Some[Int & { type Names = "years" *: EmptyTuple }] =
Some(age.hidden.asInstanceOf)

object StringExample:
def unapply(str: String): (Char, Char) & { type Names = "first" *: "\"" *: EmptyTuple } =
Some((str.head, str.last)).asInstanceOf

case class User(name: String, age: Age, city: String)

val user = User(name = "Anna", age = Age(10), city = "Berlin")

val annasCity = user match
case User(name = "Tom", city = city) => ???
case User(city = c, name = s"Ann$_") => c
case User(name = guy @ ("Guy" | "guy")) => ???

// nested patterns
val User(name = name, age = Age(years = years)) = user

// partial function
val maybeTom = Some(user).collect {
case u @ User(name = StringExample(`"` = 'm')) => u
}

val berlinerNames = for
case User(city = "Berlin", name = name) <- List(user)
yield name
1 change: 1 addition & 0 deletions tests/run/runNamedPatternMatching.check
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Guy
Loading