From fdd41ca9d3dd8101699922cfad795b7d9116db27 Mon Sep 17 00:00:00 2001 From: Jentsch Date: Wed, 23 Feb 2022 23:17:08 +0100 Subject: [PATCH 01/26] Second draft --- compiler/src/dotty/tools/dotc/ast/Trees.scala | 3 + .../src/dotty/tools/dotc/config/Feature.scala | 2 + .../dotty/tools/dotc/parsing/Parsers.scala | 15 +++- .../dotty/tools/dotc/typer/Applications.scala | 70 +++++++++++++++++-- .../tools/dotc/NamedPatternMatching.scala | 34 +++++++++ docs/_docs/reference/syntax.md | 3 +- tests/pos/namedPatternMatching.scala | 51 ++++++++++++++ 7 files changed, 168 insertions(+), 10 deletions(-) create mode 100644 compiler/test/dotty/tools/dotc/NamedPatternMatching.scala create mode 100644 tests/pos/namedPatternMatching.scala diff --git a/compiler/src/dotty/tools/dotc/ast/Trees.scala b/compiler/src/dotty/tools/dotc/ast/Trees.scala index 41077637d92e..589aea911559 100644 --- a/compiler/src/dotty/tools/dotc/ast/Trees.scala +++ b/compiler/src/dotty/tools/dotc/ast/Trees.scala @@ -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] { diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index 4a87f5b4a537..d15cc63be525 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -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) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index e8ecf1c88f8c..2ddd58636347 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -2829,10 +2829,21 @@ object Parsers { p = atSpan(startOffset(t), in.offset) { Apply(p, argumentPatterns()) } p - /** Patterns ::= Pattern [`,' Pattern] + /** Patterns ::= PatternArgument [`,' PatternArgument] */ 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.lookahead.token == EQUALS) + 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) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 04f5bf034ac0..96bbb3689456 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -136,6 +136,21 @@ object Applications { sels.takeWhile(_.exists).toList } + def indexOfNames(tp: Type)(using Context): Map[String, Int] = { + // TODO: Everything here + val r = tp.typeMembers + .filter(tpe => tpe.name.startsWith("_")) + .map(tpe => tpe.name.show -> tpe.info.asInstanceOf[TypeAlias].toString) + .collect { case (s"_$i", s"TypeBounds(ConstantType(Constant($nameAndMore") => + nameAndMore.takeWhile(_.isLetter) -> (i.toInt - 1) + } + .toMap + + println(r) + + r + } + def getUnapplySelectors(tp: Type, args: List[untpd.Tree], pos: SrcPos)(using Context): List[Type] = if (args.length > 1 && !(tp.derivesFrom(defn.SeqClass))) { val sels = productSelectorTypes(tp, pos) @@ -1377,19 +1392,60 @@ trait Applications extends Compatibility { var argTypes = unapplyArgs(unapplyApp.tpe, unapplyFn, args, tree.srcPos) for (argType <- argTypes) assert(!isBounds(argType), unapplyApp.tpe.show) - val bunchedArgs = argTypes match { + var bunchedArgs = argTypes 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 && argTypes != Nil && !bunchedArgs.head.isInstanceOf[dotty.tools.dotc.ast.Trees.NamedArg[_]]) { + unapplyPatterns += typed(bunchedArgs.head, argTypes.head) + bunchedArgs = bunchedArgs.tail + argTypes = argTypes.tail + } + + // named pattern + if (bunchedArgs != Nil && argTypes != Nil) { + + val positionOfName: Map[String, Int] = + // TODO: The test should accutally be: is unapplyFn generated by the compiler because we have a case class + if unapplyFn.tpe.widen.paramInfoss.head.head.typeSymbol.is(CaseClass) then + unapplyFn.tpe.widen.paramInfoss.head.head.fields.map(_.name.toString).zipWithIndex.toMap + else + indexOfNames(unapplyFn.tpe.widen.asInstanceOf[MethodType].resType) + + val namedArgs = bunchedArgs + // TODO: Report error if anything else than a NamedArg is in bunchedArgs + .collect { case n @ NamedArg(_, _) => n } + .groupBy { case NamedArg(name, _) => name.toString } + .map { + case (positionOfName(index), NamedArg(_, term) :: Nil) => index -> term + case (unkownName, pattern @ NamedArg(_, term) :: Nil) => + // TODO: Report position of the name + report.error(s"${unkownName.show} is unknown", tree.srcPos) + // TODO: Hack, this terms should be filtered out + -1 -> term + case error @ (_, _) => println(error); ??? + } + + while (argTypes != Nil) + // TODO: calling knownSize is maybe to slow + // TODO: Same is maybe true for the call by name argument + val term = namedArgs.getOrElse(unapplyPatterns.knownSize, { + var ignore = underscore + ignore.span = unapplyFn.span + ignore + }) + unapplyPatterns += typed(term, argTypes.head) + argTypes = argTypes.tail } - val unapplyPatterns = bunchedArgs.lazyZip(argTypes) map (typed(_, _)) - val result = assignType(cpy.UnApply(tree)(unapplyFn, unapplyImplicits(unapplyApp), unapplyPatterns), ownType) + + val result = assignType(cpy.UnApply(tree)(unapplyFn, unapplyImplicits(unapplyApp), unapplyPatterns.result), ownType) unapp.println(s"unapply patterns = $unapplyPatterns") if (ownType.stripped eq selType.stripped) || ownType.isError then result else tryWithTypeTest(Typed(result, TypeTree(ownType)), selType) diff --git a/compiler/test/dotty/tools/dotc/NamedPatternMatching.scala b/compiler/test/dotty/tools/dotc/NamedPatternMatching.scala new file mode 100644 index 000000000000..24eaf69f0ef8 --- /dev/null +++ b/compiler/test/dotty/tools/dotc/NamedPatternMatching.scala @@ -0,0 +1,34 @@ +package dotty +package tools +package dotc + +import org.junit.{ Test, BeforeClass, AfterClass } +import org.junit.Assert._ +import org.junit.Assume._ +import org.junit.experimental.categories.Category + +import java.io.File +import java.nio.file._ +import java.util.stream.{ Stream => JStream } +import scala.collection.JavaConverters._ +import scala.util.matching.Regex +import scala.concurrent.duration._ +import TestSources.sources +import vulpix._ + +class NamedPatternMatching { + import ParallelTesting._ + import TestConfiguration._ + import CompilationTests._ + import CompilationTest.aggregateTests + + // Positive tests ------------------------------------------------------------ + + @Test def pos: Unit = { + implicit val testGroup: TestGroup = TestGroup("compilePos") + aggregateTests( + compileFile("tests/pos/namedPatternMatching.scala", defaultOptions), + ).checkCompile() + } + +} diff --git a/docs/_docs/reference/syntax.md b/docs/_docs/reference/syntax.md index cabb9d9546ae..70439799dcc5 100644 --- a/docs/_docs/reference/syntax.md +++ b/docs/_docs/reference/syntax.md @@ -318,9 +318,10 @@ SimplePattern1 ::= SimpleRef | SimplePattern1 ‘.’ id PatVar ::= varid | ‘_’ -Patterns ::= Pattern {‘,’ Pattern} +Patterns ::= NamedPattern {‘,’ NamedPattern} ArgumentPatterns ::= ‘(’ [Patterns] ‘)’ | ‘(’ [Patterns ‘,’] PatVar ‘*’ ‘)’ +NamedPattern ::= [id ‘=’] Pattern ``` ### Type and Value Parameters diff --git a/tests/pos/namedPatternMatching.scala b/tests/pos/namedPatternMatching.scala new file mode 100644 index 000000000000..461937695540 --- /dev/null +++ b/tests/pos/namedPatternMatching.scala @@ -0,0 +1,51 @@ +package patterns + +class Age(val hidden: Int) + +object Age: + def apply(years: Int): Age = new Age(years) + + case class UnapplyAge(_1: Int) { + type _1 = "years" + inline def _years = _1 + type names = "years" +: () + } + + // TODO: Describe alternative encoding with tagged tuples + def unapply(age: Age): UnapplyAge = UnapplyAge(age.hidden) + +// TODO: Doesn't work. indexOfNames doesn't extract the correct names +object StringExample: + def unapply(str: String): { type _1 = "first"; type _2 = "last"} & Option[(Char, Char)] = + 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 funtion +val maybeTom = Some(user).collect { + case u @ User(name = "Tom") => u +} + +val berlinerNames = for + case User(city = "Berlin", name = name) <- List(user) +yield name + +@main +def main(): Unit = + println(annasCity) + println(name) + println(years) + + println(maybeTom) + println(berlinerNames) + From 9f9ee6539ff76f047319e0bbb1e0ebf47fa5834a Mon Sep 17 00:00:00 2001 From: Jentsch Date: Thu, 24 Feb 2022 01:32:23 +0100 Subject: [PATCH 02/26] Changed encoding of named pattern matching --- .../dotty/tools/dotc/typer/Applications.scala | 19 ++++++++++--------- tests/pos/namedPatternMatching.scala | 15 +++++---------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 96bbb3689456..419ccad5a91f 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -136,19 +136,18 @@ object Applications { sels.takeWhile(_.exists).toList } + def indexOfNames(tp: Type)(using Context): Map[String, Int] = { // TODO: Everything here - val r = tp.typeMembers - .filter(tpe => tpe.name.startsWith("_")) - .map(tpe => tpe.name.show -> tpe.info.asInstanceOf[TypeAlias].toString) - .collect { case (s"_$i", s"TypeBounds(ConstantType(Constant($nameAndMore") => - nameAndMore.takeWhile(_.isLetter) -> (i.toInt - 1) - } - .toMap + val ExprType(AndType(_, RefinedType(_, names, TypeBounds(lower, _)))) = tp.member(nme.get).info + assert(names.toString == "Names") - println(r) + val reg = "Constant\\(([^\\)]*)".r - r + reg.findAllMatchIn(lower.toString) + .map(_.group(1)) + .zipWithIndex + .toMap } def getUnapplySelectors(tp: Type, args: List[untpd.Tree], pos: SrcPos)(using Context): List[Type] = @@ -1410,9 +1409,11 @@ trait Applications extends Compatibility { } // named pattern + // TODO: Maybe the reorder method above can be reused, or be template if (bunchedArgs != Nil && argTypes != Nil) { val positionOfName: Map[String, Int] = + // TODO: Prefere explicitly declared names, check that type member 'Names' exsists // TODO: The test should accutally be: is unapplyFn generated by the compiler because we have a case class if unapplyFn.tpe.widen.paramInfoss.head.head.typeSymbol.is(CaseClass) then unapplyFn.tpe.widen.paramInfoss.head.head.fields.map(_.name.toString).zipWithIndex.toMap diff --git a/tests/pos/namedPatternMatching.scala b/tests/pos/namedPatternMatching.scala index 461937695540..26a748deca98 100644 --- a/tests/pos/namedPatternMatching.scala +++ b/tests/pos/namedPatternMatching.scala @@ -5,18 +5,13 @@ class Age(val hidden: Int) object Age: def apply(years: Int): Age = new Age(years) - case class UnapplyAge(_1: Int) { - type _1 = "years" - inline def _years = _1 - type names = "years" +: () - } - // TODO: Describe alternative encoding with tagged tuples - def unapply(age: Age): UnapplyAge = UnapplyAge(age.hidden) + def unapply(age: Age): Some[Int & { type Names = "years" *: EmptyTuple }] = + Some(age.hidden.asInstanceOf) // TODO: Doesn't work. indexOfNames doesn't extract the correct names object StringExample: - def unapply(str: String): { type _1 = "first"; type _2 = "last"} & Option[(Char, Char)] = + def unapply(str: String): Option[(Char, Char) & { type Names = "first" *: "last" *: EmptyTuple }] = Some((str.head, str.last)).asInstanceOf case class User(name: String, age: Age, city: String) @@ -31,9 +26,9 @@ val annasCity = user match // nested patterns val User(name = name, age = Age(years = years)) = user -// partial funtion +// partial function val maybeTom = Some(user).collect { - case u @ User(name = "Tom") => u + case u @ User(name = StringExample(last = 'm')) => u } val berlinerNames = for From ed07f1205517f0d388a0339c5eead85898794dce Mon Sep 17 00:00:00 2001 From: Jentsch Date: Fri, 25 Feb 2022 15:33:27 +0100 Subject: [PATCH 03/26] Resolve prefer user defined names above case class names --- .../dotty/tools/dotc/typer/Applications.scala | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 419ccad5a91f..ca63827f5684 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -136,20 +136,6 @@ object Applications { sels.takeWhile(_.exists).toList } - - def indexOfNames(tp: Type)(using Context): Map[String, Int] = { - // TODO: Everything here - val ExprType(AndType(_, RefinedType(_, names, TypeBounds(lower, _)))) = tp.member(nme.get).info - assert(names.toString == "Names") - - val reg = "Constant\\(([^\\)]*)".r - - reg.findAllMatchIn(lower.toString) - .map(_.group(1)) - .zipWithIndex - .toMap - } - def getUnapplySelectors(tp: Type, args: List[untpd.Tree], pos: SrcPos)(using Context): List[Type] = if (args.length > 1 && !(tp.derivesFrom(defn.SeqClass))) { val sels = productSelectorTypes(tp, pos) @@ -1409,16 +1395,30 @@ trait Applications extends Compatibility { } // named pattern - // TODO: Maybe the reorder method above can be reused, or be template + // TODO: Maybe the 'reorder' method above can be reused, or be template if (bunchedArgs != Nil && argTypes != Nil) { + val typeInfoOfGetMethod = unapplyFn.tpe.widen.asInstanceOf[MethodType].resType.member(nme.get).info + + val names = typeInfoOfGetMethod + .memberDenots(typeNameFilter, (name, buf) => if (name.toString == "Names") buf += typeInfoOfGetMethod.member(name).asSingleDenotation) + .headOption + val positionOfName: Map[String, Int] = - // TODO: Prefere explicitly declared names, check that type member 'Names' exsists + if names.isDefined then + // TODO: Don't use regular expression to deconstruct tuple + val reg = "\\(\"([^\"]*)\" : String\\)".r + reg.findAllMatchIn(names.get.showDcl) + .map(_.group(1)) + .zipWithIndex + .toMap // TODO: The test should accutally be: is unapplyFn generated by the compiler because we have a case class - if unapplyFn.tpe.widen.paramInfoss.head.head.typeSymbol.is(CaseClass) then + 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 - indexOfNames(unapplyFn.tpe.widen.asInstanceOf[MethodType].resType) + report.error("Doesn't support named patterns") + Map.empty + val namedArgs = bunchedArgs // TODO: Report error if anything else than a NamedArg is in bunchedArgs From cf2b7ba777c5b2532b008b9a5e2d5dd26a470e9f Mon Sep 17 00:00:00 2001 From: Jentsch Date: Wed, 16 Mar 2022 16:28:06 +0100 Subject: [PATCH 04/26] Small improvments --- compiler/src/dotty/tools/dotc/typer/Applications.scala | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index ca63827f5684..398af57c45f4 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -1412,23 +1412,22 @@ trait Applications extends Compatibility { .map(_.group(1)) .zipWithIndex .toMap - // TODO: The test should accutally be: is unapplyFn generated by the compiler because we have a case class + // TODO: The test should accutally 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("Doesn't support named patterns") Map.empty - val namedArgs = bunchedArgs // TODO: Report error if anything else than a NamedArg is in bunchedArgs .collect { case n @ NamedArg(_, _) => n } - .groupBy { case NamedArg(name, _) => name.toString } + .groupBy { case NamedArg(name, _) => name.show } .map { case (positionOfName(index), NamedArg(_, term) :: Nil) => index -> term - case (unkownName, pattern @ NamedArg(_, term) :: Nil) => + case (unkownName, (pattern @ NamedArg(_, term)) :: Nil) => // TODO: Report position of the name - report.error(s"${unkownName.show} is unknown", tree.srcPos) + report.error(s"${unkownName.show} is unknown", pattern) // TODO: Hack, this terms should be filtered out -1 -> term case error @ (_, _) => println(error); ??? From f5d5b33724235dcc444972725aabfaef905e4ab0 Mon Sep 17 00:00:00 2001 From: Jentsch Date: Wed, 8 Jun 2022 22:04:56 +0200 Subject: [PATCH 05/26] Improve error reporting --- .../dotty/tools/dotc/typer/Applications.scala | 13 ++++--- .../tools/dotc/NamedPatternMatching.scala | 8 ++++- tests/neg/negNamedPatternMatching.scala | 34 +++++++++++++++++++ tests/pos/namedPatternMatching.scala | 11 ------ 4 files changed, 50 insertions(+), 16 deletions(-) create mode 100644 tests/neg/negNamedPatternMatching.scala diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 398af57c45f4..de803e6ad6a6 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -1407,7 +1407,7 @@ trait Applications extends Compatibility { val positionOfName: Map[String, Int] = if names.isDefined then // TODO: Don't use regular expression to deconstruct tuple - val reg = "\\(\"([^\"]*)\" : String\\)".r + val reg = "\"([^\"]+)\"".r reg.findAllMatchIn(names.get.showDcl) .map(_.group(1)) .zipWithIndex @@ -1416,18 +1416,23 @@ trait Applications extends Compatibility { 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 + // TODO: Report member report.error("Doesn't support named patterns") Map.empty val namedArgs = bunchedArgs - // TODO: Report error if anything else than a NamedArg is in bunchedArgs - .collect { case n @ NamedArg(_, _) => n } + .flatMap { + case n @ NamedArg(_, _) => Seq(n) + case unnamedArgument => + report.error("Only named arguments allowed here", unnamedArgument) + Seq.empty + } .groupBy { case NamedArg(name, _) => name.show } .map { case (positionOfName(index), NamedArg(_, term) :: Nil) => index -> term case (unkownName, (pattern @ NamedArg(_, term)) :: Nil) => // TODO: Report position of the name - report.error(s"${unkownName.show} is unknown", pattern) + report.error(s"'${unkownName.show}' is unknown", pattern) // TODO: Hack, this terms should be filtered out -1 -> term case error @ (_, _) => println(error); ??? diff --git a/compiler/test/dotty/tools/dotc/NamedPatternMatching.scala b/compiler/test/dotty/tools/dotc/NamedPatternMatching.scala index 24eaf69f0ef8..aa1956853293 100644 --- a/compiler/test/dotty/tools/dotc/NamedPatternMatching.scala +++ b/compiler/test/dotty/tools/dotc/NamedPatternMatching.scala @@ -10,7 +10,6 @@ import org.junit.experimental.categories.Category import java.io.File import java.nio.file._ import java.util.stream.{ Stream => JStream } -import scala.collection.JavaConverters._ import scala.util.matching.Regex import scala.concurrent.duration._ import TestSources.sources @@ -31,4 +30,11 @@ class NamedPatternMatching { ).checkCompile() } + @Test def negativeTests: Unit = { + implicit val testGroup: TestGroup = TestGroup("compileNeg") + aggregateTests( + compileFile("tests/neg/negNamedPatternMatching.scala", defaultOptions), + ).checkExpectedErrors() + } + } diff --git a/tests/neg/negNamedPatternMatching.scala b/tests/neg/negNamedPatternMatching.scala new file mode 100644 index 000000000000..4b128f831e72 --- /dev/null +++ b/tests/neg/negNamedPatternMatching.scala @@ -0,0 +1,34 @@ +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): Option[(Char, Char) & { type Names = "first" *: "last" *: 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(names = "Tom", city = city) => ??? // error + case User(city = _, 10) => null // error + +// nested patterns +val User(name = name, age = Age(years = years)) = user + +// partial function +val maybeTom = Some(user).collect { + case u @ User(name = StringExample(last = 'm')) => u +} + +val berlinerNames = for + case User(city = "Berlin", name = name) <- List(user) +yield name diff --git a/tests/pos/namedPatternMatching.scala b/tests/pos/namedPatternMatching.scala index 26a748deca98..961e1581f98a 100644 --- a/tests/pos/namedPatternMatching.scala +++ b/tests/pos/namedPatternMatching.scala @@ -9,7 +9,6 @@ object Age: def unapply(age: Age): Some[Int & { type Names = "years" *: EmptyTuple }] = Some(age.hidden.asInstanceOf) -// TODO: Doesn't work. indexOfNames doesn't extract the correct names object StringExample: def unapply(str: String): Option[(Char, Char) & { type Names = "first" *: "last" *: EmptyTuple }] = Some((str.head, str.last)).asInstanceOf @@ -34,13 +33,3 @@ val maybeTom = Some(user).collect { val berlinerNames = for case User(city = "Berlin", name = name) <- List(user) yield name - -@main -def main(): Unit = - println(annasCity) - println(name) - println(years) - - println(maybeTom) - println(berlinerNames) - From db69a3a51cacfd2cd13d7f59805b55b5d59ef85f Mon Sep 17 00:00:00 2001 From: Jentsch Date: Wed, 8 Jun 2022 22:43:38 +0200 Subject: [PATCH 06/26] Remove hack --- .../dotty/tools/dotc/typer/Applications.scala | 16 ++++++++-------- tests/neg/negNamedPatternMatching.check | 8 ++++++++ 2 files changed, 16 insertions(+), 8 deletions(-) create mode 100644 tests/neg/negNamedPatternMatching.check diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index de803e6ad6a6..105fcd6e0247 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -1404,7 +1404,7 @@ trait Applications extends Compatibility { .memberDenots(typeNameFilter, (name, buf) => if (name.toString == "Names") buf += typeInfoOfGetMethod.member(name).asSingleDenotation) .headOption - val positionOfName: Map[String, Int] = + val positionOfName: PartialFunction[Name, Int] = if names.isDefined then // TODO: Don't use regular expression to deconstruct tuple val reg = "\"([^\"]+)\"".r @@ -1419,22 +1419,22 @@ trait Applications extends Compatibility { // TODO: Report member report.error("Doesn't support named patterns") Map.empty + .compose{ case name: Name => name.show } val namedArgs = bunchedArgs .flatMap { - case n @ NamedArg(_, _) => Seq(n) + case pattern @ NamedArg(positionOfName(_), _) => Seq(pattern) + case pattern @ NamedArg(unkownName, _) => + // TODO: Report position of the name + report.error(s"'${unkownName.show}' is unknown", pattern) + Seq.empty case unnamedArgument => report.error("Only named arguments allowed here", unnamedArgument) Seq.empty } .groupBy { case NamedArg(name, _) => name.show } .map { - case (positionOfName(index), NamedArg(_, term) :: Nil) => index -> term - case (unkownName, (pattern @ NamedArg(_, term)) :: Nil) => - // TODO: Report position of the name - report.error(s"'${unkownName.show}' is unknown", pattern) - // TODO: Hack, this terms should be filtered out - -1 -> term + case (_, NamedArg(positionOfName(index), term) :: Nil) => index -> term case error @ (_, _) => println(error); ??? } diff --git a/tests/neg/negNamedPatternMatching.check b/tests/neg/negNamedPatternMatching.check new file mode 100644 index 000000000000..959047158bce --- /dev/null +++ b/tests/neg/negNamedPatternMatching.check @@ -0,0 +1,8 @@ +-- Error: tests/neg/negNamedPatternMatching.scala:21:20 ---------------------------------------------------------------- +21 | case User(names = "Tom", city = city) => ??? // error: constructor is private + | ^^^^^ + | 'names' is unknown +-- Error: tests/neg/negNamedPatternMatching.scala:22:22 ---------------------------------------------------------------- +22 | case User(city = _, 10) => null // error + | ^^ + | Only named arguments allowed here From 24f8b7b8a525da58c4062627ced553035deaec6d Mon Sep 17 00:00:00 2001 From: Jentsch Date: Thu, 9 Jun 2022 00:23:25 +0200 Subject: [PATCH 07/26] Add handling of duplicated names --- .../dotty/tools/dotc/typer/Applications.scala | 18 ++++++++++-------- tests/neg/negNamedPatternMatching.check | 10 +++++++++- tests/neg/negNamedPatternMatching.scala | 17 +++++------------ 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 105fcd6e0247..6f88224d28e2 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -1395,6 +1395,8 @@ trait Applications extends Compatibility { } // named pattern + // TODO: Errors report the wrong position + // TODO: Use propper error reporting // TODO: Maybe the 'reorder' method above can be reused, or be template if (bunchedArgs != Nil && argTypes != Nil) { @@ -1406,7 +1408,7 @@ trait Applications extends Compatibility { val positionOfName: PartialFunction[Name, Int] = if names.isDefined then - // TODO: Don't use regular expression to deconstruct tuple + // TODO: Don't use regular expression to deconstruct tuples val reg = "\"([^\"]+)\"".r reg.findAllMatchIn(names.get.showDcl) .map(_.group(1)) @@ -1425,18 +1427,19 @@ trait Applications extends Compatibility { .flatMap { case pattern @ NamedArg(positionOfName(_), _) => Seq(pattern) case pattern @ NamedArg(unkownName, _) => - // TODO: Report position of the name report.error(s"'${unkownName.show}' is unknown", pattern) Seq.empty case unnamedArgument => report.error("Only named arguments allowed here", unnamedArgument) Seq.empty } - .groupBy { case NamedArg(name, _) => name.show } - .map { - case (_, NamedArg(positionOfName(index), term) :: Nil) => index -> term - case error @ (_, _) => println(error); ??? - } + .groupMapReduce(arg => positionOfName(arg.name))(identity)( + (first, second) => { + // TODO: Document design decission here + report.error(s"'${second.name}' was already used before", second); + first + }) + while (argTypes != Nil) // TODO: calling knownSize is maybe to slow @@ -1451,7 +1454,6 @@ trait Applications extends Compatibility { } val result = assignType(cpy.UnApply(tree)(unapplyFn, unapplyImplicits(unapplyApp), unapplyPatterns.result), ownType) - unapp.println(s"unapply patterns = $unapplyPatterns") if (ownType.stripped eq selType.stripped) || ownType.isError then result else tryWithTypeTest(Typed(result, TypeTree(ownType)), selType) case tp => diff --git a/tests/neg/negNamedPatternMatching.check b/tests/neg/negNamedPatternMatching.check index 959047158bce..c145fc7c6090 100644 --- a/tests/neg/negNamedPatternMatching.check +++ b/tests/neg/negNamedPatternMatching.check @@ -1,8 +1,16 @@ -- Error: tests/neg/negNamedPatternMatching.scala:21:20 ---------------------------------------------------------------- -21 | case User(names = "Tom", city = city) => ??? // error: constructor is private +21 | case User(names = "Tom", city = city) => ??? // error | ^^^^^ | 'names' is unknown -- Error: tests/neg/negNamedPatternMatching.scala:22:22 ---------------------------------------------------------------- 22 | case User(city = _, 10) => null // error | ^^ | Only named arguments allowed here +-- Error: tests/neg/negNamedPatternMatching.scala:25:11 ---------------------------------------------------------------- +25 | name = "Tom 2", // error + | ^^^^^^^ + | 'name' was already used before +-- Error: tests/neg/negNamedPatternMatching.scala:26:11 ---------------------------------------------------------------- +26 | name = "Tom 3" // error + | ^^^^^^^ + | 'name' was already used before diff --git a/tests/neg/negNamedPatternMatching.scala b/tests/neg/negNamedPatternMatching.scala index 4b128f831e72..f86a023dd4af 100644 --- a/tests/neg/negNamedPatternMatching.scala +++ b/tests/neg/negNamedPatternMatching.scala @@ -20,15 +20,8 @@ val user = User(name = "Anna", age = Age(10), city = "Berlin") val annasCity = user match case User(names = "Tom", city = city) => ??? // error case User(city = _, 10) => null // error - -// nested patterns -val User(name = name, age = Age(years = years)) = user - -// partial function -val maybeTom = Some(user).collect { - case u @ User(name = StringExample(last = 'm')) => u -} - -val berlinerNames = for - case User(city = "Berlin", name = name) <- List(user) -yield name + case User( + name = "Tom", + name = "Tom 2", // error + name = "Tom 3" // error + ) => null \ No newline at end of file From 5b10bf7f145c93566f1fe7dd8af9db4bf3f4972d Mon Sep 17 00:00:00 2001 From: Jentsch Date: Thu, 9 Jun 2022 01:30:10 +0200 Subject: [PATCH 08/26] Spelling --- .../src/dotty/tools/dotc/typer/Applications.scala | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 6f88224d28e2..d865e2aaf23e 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -1396,7 +1396,7 @@ trait Applications extends Compatibility { // named pattern // TODO: Errors report the wrong position - // TODO: Use propper error reporting + // TODO: Use proper error reporting // TODO: Maybe the 'reorder' method above can be reused, or be template if (bunchedArgs != Nil && argTypes != Nil) { @@ -1414,20 +1414,21 @@ trait Applications extends Compatibility { .map(_.group(1)) .zipWithIndex .toMap - // TODO: The test should accutally be: is unapplyFn generated by the compiler because we have a case class? + // 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 // TODO: Report member report.error("Doesn't support named patterns") Map.empty + // TODO: Is is necessary to convert names to strings? .compose{ case name: Name => name.show } val namedArgs = bunchedArgs .flatMap { case pattern @ NamedArg(positionOfName(_), _) => Seq(pattern) - case pattern @ NamedArg(unkownName, _) => - report.error(s"'${unkownName.show}' is unknown", pattern) + case pattern @ NamedArg(unknownName, _) => + report.error(s"'${unknownName.show}' is unknown", pattern) Seq.empty case unnamedArgument => report.error("Only named arguments allowed here", unnamedArgument) @@ -1435,7 +1436,7 @@ trait Applications extends Compatibility { } .groupMapReduce(arg => positionOfName(arg.name))(identity)( (first, second) => { - // TODO: Document design decission here + // TODO: Document design decision here report.error(s"'${second.name}' was already used before", second); first }) From 7cca3e80a93f4c2f9b57b0a0627287419a15b4a9 Mon Sep 17 00:00:00 2001 From: Jentsch Date: Thu, 9 Jun 2022 01:46:03 +0200 Subject: [PATCH 09/26] Report member that doesn't support named arguments --- .../dotty/tools/dotc/typer/Applications.scala | 34 ++++++++++--------- tests/neg/negNamedPatternMatching.check | 20 ++++++----- tests/neg/negNamedPatternMatching.scala | 14 ++++---- 3 files changed, 36 insertions(+), 32 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index d865e2aaf23e..e514405a33f1 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -1406,29 +1406,31 @@ trait Applications extends Compatibility { .memberDenots(typeNameFilter, (name, buf) => if (name.toString == "Names") buf += typeInfoOfGetMethod.member(name).asSingleDenotation) .headOption + val positionOfStringNames: Map[String, Int] = + if names.isDefined then + // TODO: Don't use regular expression to deconstruct tuples + val reg = "\"([^\"]+)\"".r + reg.findAllMatchIn(names.get.showDcl) + .map(_.group(1)) + .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] = - if names.isDefined then - // TODO: Don't use regular expression to deconstruct tuples - val reg = "\"([^\"]+)\"".r - reg.findAllMatchIn(names.get.showDcl) - .map(_.group(1)) - .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 - // TODO: Report member - report.error("Doesn't support named patterns") - Map.empty // TODO: Is is necessary to convert names to strings? - .compose{ case name: Name => name.show } + positionOfStringNames.compose(_.show) val namedArgs = bunchedArgs .flatMap { case pattern @ NamedArg(positionOfName(_), _) => Seq(pattern) case pattern @ NamedArg(unknownName, _) => - report.error(s"'${unknownName.show}' is unknown", pattern) + if (positionOfStringNames.nonEmpty) + report.error(s"'${unknownName.show}' is unknown", pattern) Seq.empty case unnamedArgument => report.error("Only named arguments allowed here", unnamedArgument) diff --git a/tests/neg/negNamedPatternMatching.check b/tests/neg/negNamedPatternMatching.check index c145fc7c6090..08edbe1cd7d7 100644 --- a/tests/neg/negNamedPatternMatching.check +++ b/tests/neg/negNamedPatternMatching.check @@ -1,16 +1,20 @@ --- Error: tests/neg/negNamedPatternMatching.scala:21:20 ---------------------------------------------------------------- -21 | case User(names = "Tom", city = city) => ??? // error +-- Error: tests/neg/negNamedPatternMatching.scala:18:20 ---------------------------------------------------------------- +18 | case User(names = "Tom", city = city) => ??? // error | ^^^^^ | 'names' is unknown --- Error: tests/neg/negNamedPatternMatching.scala:22:22 ---------------------------------------------------------------- -22 | case User(city = _, 10) => null // error +-- Error: tests/neg/negNamedPatternMatching.scala:19:22 ---------------------------------------------------------------- +19 | case User(city = _, 10) => null // error | ^^ | Only named arguments allowed here --- Error: tests/neg/negNamedPatternMatching.scala:25:11 ---------------------------------------------------------------- -25 | name = "Tom 2", // error +-- Error: tests/neg/negNamedPatternMatching.scala:20:18 ---------------------------------------------------------------- +20 | case User(age = Age(years = 10)) => null // error + | ^^^ + | 'patterns.Age' doesn't support named patterns +-- Error: tests/neg/negNamedPatternMatching.scala:23:11 ---------------------------------------------------------------- +23 | name = "Tom 2", // error | ^^^^^^^ | 'name' was already used before --- Error: tests/neg/negNamedPatternMatching.scala:26:11 ---------------------------------------------------------------- -26 | name = "Tom 3" // error +-- Error: tests/neg/negNamedPatternMatching.scala:24:11 ---------------------------------------------------------------- +24 | name = "Tom 3" // error | ^^^^^^^ | 'name' was already used before diff --git a/tests/neg/negNamedPatternMatching.scala b/tests/neg/negNamedPatternMatching.scala index f86a023dd4af..1d23db811f33 100644 --- a/tests/neg/negNamedPatternMatching.scala +++ b/tests/neg/negNamedPatternMatching.scala @@ -1,17 +1,14 @@ package patterns -class Age(val hidden: Int) +type Age = Age.Age object Age: - def apply(years: Int): Age = new Age(years) + opaque type Age = Int - // TODO: Describe alternative encoding with tagged tuples - def unapply(age: Age): Some[Int & { type Names = "years" *: EmptyTuple }] = - Some(age.hidden.asInstanceOf) + def apply(years: Int): Age = years -object StringExample: - def unapply(str: String): Option[(Char, Char) & { type Names = "first" *: "last" *: EmptyTuple }] = - Some((str.head, str.last)).asInstanceOf + def unapply(age: Age): Some[Int] = + Some(age) case class User(name: String, age: Age, city: String) @@ -20,6 +17,7 @@ val user = User(name = "Anna", age = Age(10), city = "Berlin") val annasCity = user match case User(names = "Tom", city = city) => ??? // error case User(city = _, 10) => null // error + case User(age = Age(years = 10)) => null // error case User( name = "Tom", name = "Tom 2", // error From 207e463b4c3d6c9ab453c5b7f963df12600e130b Mon Sep 17 00:00:00 2001 From: Jentsch Date: Thu, 9 Jun 2022 02:10:37 +0200 Subject: [PATCH 10/26] Add more error handling --- compiler/src/dotty/tools/dotc/typer/Applications.scala | 7 ++++--- tests/neg/negNamedPatternMatching.check | 4 ++++ tests/neg/negNamedPatternMatching.scala | 3 ++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index e514405a33f1..468ad271aa96 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -1395,7 +1395,7 @@ trait Applications extends Compatibility { } // named pattern - // TODO: Errors report the wrong position + // 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 && argTypes != Nil) { @@ -1427,6 +1427,9 @@ trait Applications extends Compatibility { 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) @@ -1445,8 +1448,6 @@ trait Applications extends Compatibility { while (argTypes != Nil) - // TODO: calling knownSize is maybe to slow - // TODO: Same is maybe true for the call by name argument val term = namedArgs.getOrElse(unapplyPatterns.knownSize, { var ignore = underscore ignore.span = unapplyFn.span diff --git a/tests/neg/negNamedPatternMatching.check b/tests/neg/negNamedPatternMatching.check index 08edbe1cd7d7..602391c26917 100644 --- a/tests/neg/negNamedPatternMatching.check +++ b/tests/neg/negNamedPatternMatching.check @@ -18,3 +18,7 @@ 24 | name = "Tom 3" // error | ^^^^^^^ | 'name' was already used before +-- Error: tests/neg/negNamedPatternMatching.scala:26:22 ---------------------------------------------------------------- +26 | case User(_, name = "Anna") => null // error + | ^^^^^^ + | name was already used as a positional pattern diff --git a/tests/neg/negNamedPatternMatching.scala b/tests/neg/negNamedPatternMatching.scala index 1d23db811f33..ef68d4cc7071 100644 --- a/tests/neg/negNamedPatternMatching.scala +++ b/tests/neg/negNamedPatternMatching.scala @@ -22,4 +22,5 @@ val annasCity = user match name = "Tom", name = "Tom 2", // error name = "Tom 3" // error - ) => null \ No newline at end of file + ) => null + case User(_, name = "Anna") => null // error \ No newline at end of file From 73a2e5afe9fa65a044fa77081b11d328a5b9718d Mon Sep 17 00:00:00 2001 From: Jentsch Date: Thu, 9 Jun 2022 12:59:09 +0200 Subject: [PATCH 11/26] Add optionless support --- .../dotty/tools/dotc/typer/Applications.scala | 6 ++++- .../tools/dotc/NamedPatternMatching.scala | 7 ++++++ tests/run/runNamedPatternMatching.check | 3 +++ tests/run/runNamedPatternMatching.scala | 24 +++++++++++++++++++ 4 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 tests/run/runNamedPatternMatching.check create mode 100644 tests/run/runNamedPatternMatching.scala diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 468ad271aa96..faece28c332f 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -1400,7 +1400,11 @@ trait Applications extends Compatibility { // TODO: Maybe the 'reorder' method above can be reused, or be template if (bunchedArgs != Nil && argTypes != Nil) { - val typeInfoOfGetMethod = unapplyFn.tpe.widen.asInstanceOf[MethodType].resType.member(nme.get).info + val typeInfoOfGetMethod = + if unapplyFn.tpe.widen.asInstanceOf[MethodType].resType.member(nme.get).exists then + unapplyFn.tpe.widen.asInstanceOf[MethodType].resType.member(nme.get).info + else + unapplyFn.tpe.widen.asInstanceOf[MethodType].resType val names = typeInfoOfGetMethod .memberDenots(typeNameFilter, (name, buf) => if (name.toString == "Names") buf += typeInfoOfGetMethod.member(name).asSingleDenotation) diff --git a/compiler/test/dotty/tools/dotc/NamedPatternMatching.scala b/compiler/test/dotty/tools/dotc/NamedPatternMatching.scala index aa1956853293..9340c3ef341f 100644 --- a/compiler/test/dotty/tools/dotc/NamedPatternMatching.scala +++ b/compiler/test/dotty/tools/dotc/NamedPatternMatching.scala @@ -37,4 +37,11 @@ class NamedPatternMatching { ).checkExpectedErrors() } + @Test def executionTest: Unit = { + implicit val testGroup: TestGroup = TestGroup("runPos") + aggregateTests( + compileFile("tests/run/runNamedPatternMatching.scala", defaultOptions), + ).checkRuns() + } + } diff --git a/tests/run/runNamedPatternMatching.check b/tests/run/runNamedPatternMatching.check new file mode 100644 index 000000000000..4f8fc791d8b6 --- /dev/null +++ b/tests/run/runNamedPatternMatching.check @@ -0,0 +1,3 @@ +Got name +Got age +Got city diff --git a/tests/run/runNamedPatternMatching.scala b/tests/run/runNamedPatternMatching.scala new file mode 100644 index 000000000000..19906b69012b --- /dev/null +++ b/tests/run/runNamedPatternMatching.scala @@ -0,0 +1,24 @@ +object Test: + + class User(val name: String, val age: Int, val city: String) + + //TODO: Test this + object UserEx: + class UserExtractor(user: User) extends Product: + def _1 = println("Got name"); user.name + def _2 = println("Got age"); user.age + def _3 = println("Got city"); user.city + + type Names = ("name", "age", "city") + + // Members declared in scala.Equals + def canEqual(that: Any): Boolean = ??? + + // Members declared in scala.Product + def productArity: Int = ??? + def productElement(n: Int): Any = ??? + + def unapply(user: User) = UserExtractor(user) + + def main(args: Array[String]): Unit = + val UserEx(city = _, name = _) = User("Guy", 25, "Paris") From b1e19d30e75e59962b93c4354c1c3801408f9b1e Mon Sep 17 00:00:00 2001 From: Jentsch Date: Thu, 9 Jun 2022 13:20:11 +0200 Subject: [PATCH 12/26] Remove double lookup --- compiler/src/dotty/tools/dotc/typer/Applications.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index faece28c332f..73b7f66b2217 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -1400,11 +1400,11 @@ trait Applications extends Compatibility { // TODO: Maybe the 'reorder' method above can be reused, or be template if (bunchedArgs != Nil && argTypes != Nil) { - val typeInfoOfGetMethod = - if unapplyFn.tpe.widen.asInstanceOf[MethodType].resType.member(nme.get).exists then - unapplyFn.tpe.widen.asInstanceOf[MethodType].resType.member(nme.get).info - else - unapplyFn.tpe.widen.asInstanceOf[MethodType].resType + val resTypeOfUnapplyFn = unapplyFn.tpe.widen.asInstanceOf[MethodType].resType + + val typeInfoOfGetMethod: Type = + resTypeOfUnapplyFn.member(nme.get).info + .orElse(resTypeOfUnapplyFn) val names = typeInfoOfGetMethod .memberDenots(typeNameFilter, (name, buf) => if (name.toString == "Names") buf += typeInfoOfGetMethod.member(name).asSingleDenotation) From 25777c14f844f6b62a73800c0531acf13baab6c0 Mon Sep 17 00:00:00 2001 From: Jentsch Date: Thu, 9 Jun 2022 13:34:38 +0200 Subject: [PATCH 13/26] Use member lookup for Names --- compiler/src/dotty/tools/dotc/core/StdNames.scala | 2 ++ .../src/dotty/tools/dotc/typer/Applications.scala | 13 ++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 9f128a71be7b..525d0bc25888 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -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}") } diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 73b7f66b2217..b7a07af7a723 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -1402,19 +1402,18 @@ trait Applications extends Compatibility { val resTypeOfUnapplyFn = unapplyFn.tpe.widen.asInstanceOf[MethodType].resType - val typeInfoOfGetMethod: Type = + val names: SingleDenotation = resTypeOfUnapplyFn.member(nme.get).info .orElse(resTypeOfUnapplyFn) - - val names = typeInfoOfGetMethod - .memberDenots(typeNameFilter, (name, buf) => if (name.toString == "Names") buf += typeInfoOfGetMethod.member(name).asSingleDenotation) - .headOption + .member(tpnme.Names) + // TODO: Is it possible to get something else than a SingleDenotation? + .asSingleDenotation val positionOfStringNames: Map[String, Int] = - if names.isDefined then + if names.exists then // TODO: Don't use regular expression to deconstruct tuples val reg = "\"([^\"]+)\"".r - reg.findAllMatchIn(names.get.showDcl) + reg.findAllMatchIn(names.showDcl) .map(_.group(1)) .zipWithIndex .toMap From bc8265ec9c3ab37a4a008ea25a3506bcb1689b0a Mon Sep 17 00:00:00 2001 From: Jentsch Date: Thu, 9 Jun 2022 13:50:11 +0200 Subject: [PATCH 14/26] Restore error checking --- compiler/src/dotty/tools/dotc/typer/Applications.scala | 4 ++++ compiler/test/dotty/tools/dotc/NamedPatternMatching.scala | 1 + 2 files changed, 5 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index b7a07af7a723..15ae6aa1bdc0 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -1458,6 +1458,10 @@ trait Applications extends Compatibility { }) unapplyPatterns += typed(term, argTypes.head) argTypes = argTypes.tail + } else { + // Check for positional arguments + if (argTypes != Nil || bunchedArgs != Nil) + report.error(UnapplyInvalidNumberOfArguments(qual, argTypes), tree.srcPos) } val result = assignType(cpy.UnApply(tree)(unapplyFn, unapplyImplicits(unapplyApp), unapplyPatterns.result), ownType) diff --git a/compiler/test/dotty/tools/dotc/NamedPatternMatching.scala b/compiler/test/dotty/tools/dotc/NamedPatternMatching.scala index 9340c3ef341f..0c2089b02fff 100644 --- a/compiler/test/dotty/tools/dotc/NamedPatternMatching.scala +++ b/compiler/test/dotty/tools/dotc/NamedPatternMatching.scala @@ -34,6 +34,7 @@ class NamedPatternMatching { implicit val testGroup: TestGroup = TestGroup("compileNeg") aggregateTests( compileFile("tests/neg/negNamedPatternMatching.scala", defaultOptions), + compileFile("tests/neg/bad-unapplies.scala", defaultOptions), ).checkExpectedErrors() } From 4438b9bb30b8cab3d1ad464c7cc7420e7789f800 Mon Sep 17 00:00:00 2001 From: Jentsch Date: Thu, 9 Jun 2022 13:51:44 +0200 Subject: [PATCH 15/26] Remove wrong TODO --- compiler/src/dotty/tools/dotc/typer/Applications.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 15ae6aa1bdc0..4714fbd27cec 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -1444,7 +1444,6 @@ trait Applications extends Compatibility { } .groupMapReduce(arg => positionOfName(arg.name))(identity)( (first, second) => { - // TODO: Document design decision here report.error(s"'${second.name}' was already used before", second); first }) From bbeb263f9c38aa5efb2ca442028a4b2deb0c2a12 Mon Sep 17 00:00:00 2001 From: Jentsch Date: Thu, 9 Jun 2022 14:21:38 +0200 Subject: [PATCH 16/26] Fix error message --- .../dotty/tools/dotc/typer/Applications.scala | 25 ++++++++++--------- .../tools/dotc/NamedPatternMatching.scala | 4 +-- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 4714fbd27cec..a12498f781f9 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -1375,9 +1375,10 @@ trait Applications extends Compatibility { res.result() } - var argTypes = unapplyArgs(unapplyApp.tpe, unapplyFn, args, tree.srcPos) - for (argType <- argTypes) assert(!isBounds(argType), unapplyApp.tpe.show) - var 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 @@ -1388,17 +1389,17 @@ trait Applications extends Compatibility { // here add positional patterns to unapplyPatterns // TODO: isInstanceOf seems not to be the right tool - while (bunchedArgs != Nil && argTypes != Nil && !bunchedArgs.head.isInstanceOf[dotty.tools.dotc.ast.Trees.NamedArg[_]]) { - unapplyPatterns += typed(bunchedArgs.head, argTypes.head) + while (bunchedArgs != Nil && remainingArgTypes != Nil && !bunchedArgs.head.isInstanceOf[dotty.tools.dotc.ast.Trees.NamedArg[_]]) { + unapplyPatterns += typed(bunchedArgs.head, remainingArgTypes.head) bunchedArgs = bunchedArgs.tail - argTypes = argTypes.tail + remainingArgTypes = remainingArgTypes.tail } // 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 && argTypes != Nil) { + if (bunchedArgs != Nil && remainingArgTypes != Nil) { val resTypeOfUnapplyFn = unapplyFn.tpe.widen.asInstanceOf[MethodType].resType @@ -1449,18 +1450,18 @@ trait Applications extends Compatibility { }) - while (argTypes != Nil) + while (remainingArgTypes != Nil) val term = namedArgs.getOrElse(unapplyPatterns.knownSize, { var ignore = underscore ignore.span = unapplyFn.span ignore }) - unapplyPatterns += typed(term, argTypes.head) - argTypes = argTypes.tail + unapplyPatterns += typed(term, remainingArgTypes.head) + remainingArgTypes = remainingArgTypes.tail } else { // Check for positional arguments - if (argTypes != Nil || bunchedArgs != Nil) - report.error(UnapplyInvalidNumberOfArguments(qual, argTypes), tree.srcPos) + if (remainingArgTypes != Nil || bunchedArgs != Nil) + report.error(UnapplyInvalidNumberOfArguments(qual, allArgTypes), tree.srcPos) } val result = assignType(cpy.UnApply(tree)(unapplyFn, unapplyImplicits(unapplyApp), unapplyPatterns.result), ownType) diff --git a/compiler/test/dotty/tools/dotc/NamedPatternMatching.scala b/compiler/test/dotty/tools/dotc/NamedPatternMatching.scala index 0c2089b02fff..ad379e536921 100644 --- a/compiler/test/dotty/tools/dotc/NamedPatternMatching.scala +++ b/compiler/test/dotty/tools/dotc/NamedPatternMatching.scala @@ -2,7 +2,7 @@ package dotty package tools package dotc -import org.junit.{ Test, BeforeClass, AfterClass } +import org.junit.Test import org.junit.Assert._ import org.junit.Assume._ import org.junit.experimental.categories.Category @@ -12,7 +12,6 @@ import java.nio.file._ import java.util.stream.{ Stream => JStream } import scala.util.matching.Regex import scala.concurrent.duration._ -import TestSources.sources import vulpix._ class NamedPatternMatching { @@ -35,6 +34,7 @@ class NamedPatternMatching { aggregateTests( compileFile("tests/neg/negNamedPatternMatching.scala", defaultOptions), compileFile("tests/neg/bad-unapplies.scala", defaultOptions), + compileFile("tests/neg/i10757.scala", defaultOptions), ).checkExpectedErrors() } From fce673100006b9b6b4a2e28022c198fc42a35543 Mon Sep 17 00:00:00 2001 From: Jentsch Date: Thu, 9 Jun 2022 17:51:13 +0200 Subject: [PATCH 17/26] Use playground.scala --- .../tools/dotc/NamedPatternMatching.scala | 48 ------------------- .../test/dotty/tools/dotc/Playground.scala | 18 ++++++- 2 files changed, 16 insertions(+), 50 deletions(-) delete mode 100644 compiler/test/dotty/tools/dotc/NamedPatternMatching.scala diff --git a/compiler/test/dotty/tools/dotc/NamedPatternMatching.scala b/compiler/test/dotty/tools/dotc/NamedPatternMatching.scala deleted file mode 100644 index ad379e536921..000000000000 --- a/compiler/test/dotty/tools/dotc/NamedPatternMatching.scala +++ /dev/null @@ -1,48 +0,0 @@ -package dotty -package tools -package dotc - -import org.junit.Test -import org.junit.Assert._ -import org.junit.Assume._ -import org.junit.experimental.categories.Category - -import java.io.File -import java.nio.file._ -import java.util.stream.{ Stream => JStream } -import scala.util.matching.Regex -import scala.concurrent.duration._ -import vulpix._ - -class NamedPatternMatching { - import ParallelTesting._ - import TestConfiguration._ - import CompilationTests._ - import CompilationTest.aggregateTests - - // Positive tests ------------------------------------------------------------ - - @Test def pos: Unit = { - implicit val testGroup: TestGroup = TestGroup("compilePos") - aggregateTests( - compileFile("tests/pos/namedPatternMatching.scala", defaultOptions), - ).checkCompile() - } - - @Test def negativeTests: Unit = { - implicit val testGroup: TestGroup = TestGroup("compileNeg") - aggregateTests( - compileFile("tests/neg/negNamedPatternMatching.scala", defaultOptions), - compileFile("tests/neg/bad-unapplies.scala", defaultOptions), - compileFile("tests/neg/i10757.scala", defaultOptions), - ).checkExpectedErrors() - } - - @Test def executionTest: Unit = { - implicit val testGroup: TestGroup = TestGroup("runPos") - aggregateTests( - compileFile("tests/run/runNamedPatternMatching.scala", defaultOptions), - ).checkRuns() - } - -} diff --git a/compiler/test/dotty/tools/dotc/Playground.scala b/compiler/test/dotty/tools/dotc/Playground.scala index 40a24c0408cf..436b441cfb97 100644 --- a/compiler/test/dotty/tools/dotc/Playground.scala +++ b/compiler/test/dotty/tools/dotc/Playground.scala @@ -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() \ No newline at end of file From 07e8a7751debb8fd345ab309920453d93b123b1a Mon Sep 17 00:00:00 2001 From: Jentsch Date: Thu, 9 Jun 2022 20:09:18 +0200 Subject: [PATCH 18/26] Add seed for helper lib example --- tests/run/runNamedPatternMatching.check | 1 + tests/run/runNamedPatternMatching.scala | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/tests/run/runNamedPatternMatching.check b/tests/run/runNamedPatternMatching.check index 4f8fc791d8b6..ad4356f8e468 100644 --- a/tests/run/runNamedPatternMatching.check +++ b/tests/run/runNamedPatternMatching.check @@ -1,3 +1,4 @@ Got name Got age Got city +Guy diff --git a/tests/run/runNamedPatternMatching.scala b/tests/run/runNamedPatternMatching.scala index 19906b69012b..fb0c2672f311 100644 --- a/tests/run/runNamedPatternMatching.scala +++ b/tests/run/runNamedPatternMatching.scala @@ -20,5 +20,21 @@ object Test: def unapply(user: User) = UserExtractor(user) + object Helper: + val empty: EmptyTuple & { type Names = EmptyTuple } = EmptyTuple.asInstanceOf + + extension [N <: Singleton & String, V] (head: (N, V)) + def +::[Vs <: Tuple, Ns <: Tuple](tail: Vs & { type Names = Ns }) : V *: Vs & { type Names = N *: Ns }= + (head._2 *: tail).asInstanceOf + + object UserEx2: + import Helper._ + // TODO: The inferred type doesn't work and leads to a runtime exception + def unapply(user: User): (String, Int, String) & { type Names = ("name", "age", "city") } = + ("name", user.name) +:: ("age", user.age) +:: ("city", user.city) +:: empty + def main(args: Array[String]): Unit = val UserEx(city = _, name = _) = User("Guy", 25, "Paris") + val UserEx2(city = _, name = n) = User("Guy", 25, "Paris") + + println(n) From 40fda63adf20dcf5f5c9689997e8b5561dd74791 Mon Sep 17 00:00:00 2001 From: Jentsch Date: Sat, 11 Jun 2022 21:20:39 +0200 Subject: [PATCH 19/26] Add accessiblity test --- .../dotty/tools/dotc/typer/Applications.scala | 1 + tests/neg/negNamedPatternMatching.check | 28 ++++++++------- tests/neg/negNamedPatternMatching.scala | 34 ++++++++++++++++--- 3 files changed, 47 insertions(+), 16 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index a12498f781f9..2a7f2a846925 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -1407,6 +1407,7 @@ trait Applications extends Compatibility { resTypeOfUnapplyFn.member(nme.get).info .orElse(resTypeOfUnapplyFn) .member(tpnme.Names) + .accessibleFrom(selType) // TODO: Is it possible to get something else than a SingleDenotation? .asSingleDenotation diff --git a/tests/neg/negNamedPatternMatching.check b/tests/neg/negNamedPatternMatching.check index 602391c26917..9f5e61ef47c6 100644 --- a/tests/neg/negNamedPatternMatching.check +++ b/tests/neg/negNamedPatternMatching.check @@ -1,24 +1,28 @@ --- Error: tests/neg/negNamedPatternMatching.scala:18:20 ---------------------------------------------------------------- -18 | case User(names = "Tom", city = city) => ??? // error +-- Error: tests/neg/negNamedPatternMatching.scala:43:20 ---------------------------------------------------------------- +43 | case User(names = "Tom", city = city) => null // error | ^^^^^ | 'names' is unknown --- Error: tests/neg/negNamedPatternMatching.scala:19:22 ---------------------------------------------------------------- -19 | case User(city = _, 10) => null // error +-- Error: tests/neg/negNamedPatternMatching.scala:44:22 ---------------------------------------------------------------- +44 | case User(city = _, 10) => null // error | ^^ | Only named arguments allowed here --- Error: tests/neg/negNamedPatternMatching.scala:20:18 ---------------------------------------------------------------- -20 | case User(age = Age(years = 10)) => null // error +-- 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:23:11 ---------------------------------------------------------------- -23 | name = "Tom 2", // error +-- Error: tests/neg/negNamedPatternMatching.scala:48:11 ---------------------------------------------------------------- +48 | name = "Tom 2", // error | ^^^^^^^ | 'name' was already used before --- Error: tests/neg/negNamedPatternMatching.scala:24:11 ---------------------------------------------------------------- -24 | name = "Tom 3" // error +-- Error: tests/neg/negNamedPatternMatching.scala:49:11 ---------------------------------------------------------------- +49 | name = "Tom 3" // error | ^^^^^^^ | 'name' was already used before --- Error: tests/neg/negNamedPatternMatching.scala:26:22 ---------------------------------------------------------------- -26 | case User(_, name = "Anna") => null // error +-- 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 diff --git a/tests/neg/negNamedPatternMatching.scala b/tests/neg/negNamedPatternMatching.scala index ef68d4cc7071..cca15b248ab1 100644 --- a/tests/neg/negNamedPatternMatching.scala +++ b/tests/neg/negNamedPatternMatching.scala @@ -10,12 +10,37 @@ object Age: def unapply(age: Age): Some[Int] = Some(age) -case class User(name: String, age: Age, city: String) +type City = City.City -val user = User(name = "Anna", age = Age(10), city = "Berlin") +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) => ??? // error + case User(names = "Tom", city = city) => null // error case User(city = _, 10) => null // error case User(age = Age(years = 10)) => null // error case User( @@ -23,4 +48,5 @@ val annasCity = user match name = "Tom 2", // error name = "Tom 3" // error ) => null - case User(_, name = "Anna") => null // error \ No newline at end of file + case User(_, name = "Anna") => null // error + case User(city = City(name = "Berlin")) => null // error \ No newline at end of file From 4c0ce97d4d42bcff3f0afa4042d1b730e7830ba2 Mon Sep 17 00:00:00 2001 From: Jentsch Date: Sat, 11 Jun 2022 21:47:55 +0200 Subject: [PATCH 20/26] Fix test --- tests/run/runNamedPatternMatching.check | 1 + tests/run/runNamedPatternMatching.scala | 15 +++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/run/runNamedPatternMatching.check b/tests/run/runNamedPatternMatching.check index ad4356f8e468..f12af34956da 100644 --- a/tests/run/runNamedPatternMatching.check +++ b/tests/run/runNamedPatternMatching.check @@ -1,4 +1,5 @@ Got name Got age Got city +city = Paris Guy diff --git a/tests/run/runNamedPatternMatching.scala b/tests/run/runNamedPatternMatching.scala index fb0c2672f311..3c7bfc6cf219 100644 --- a/tests/run/runNamedPatternMatching.scala +++ b/tests/run/runNamedPatternMatching.scala @@ -5,9 +5,15 @@ object Test: //TODO: Test this object UserEx: class UserExtractor(user: User) extends Product: - def _1 = println("Got name"); user.name - def _2 = println("Got age"); user.age - def _3 = println("Got city"); user.city + def _1: String = + println("Got name") + return user.name + def _2: Int = + println("Got age") + user.age + def _3: String = + println("Got city") + user.city type Names = ("name", "age", "city") @@ -34,7 +40,8 @@ object Test: ("name", user.name) +:: ("age", user.age) +:: ("city", user.city) +:: empty def main(args: Array[String]): Unit = - val UserEx(city = _, name = _) = User("Guy", 25, "Paris") + val UserEx(city = c, name = _) = User("Guy", 25, "Paris") + println("city = " + c: String) val UserEx2(city = _, name = n) = User("Guy", 25, "Paris") println(n) From c23d4c51a434ff5e50ace7244ab3b8dd4236bc11 Mon Sep 17 00:00:00 2001 From: Jentsch Date: Sat, 11 Jun 2022 21:59:23 +0200 Subject: [PATCH 21/26] Test more excoting types --- tests/run/runNamedPatternMatching.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/run/runNamedPatternMatching.scala b/tests/run/runNamedPatternMatching.scala index 3c7bfc6cf219..a1def4d3f92f 100644 --- a/tests/run/runNamedPatternMatching.scala +++ b/tests/run/runNamedPatternMatching.scala @@ -2,8 +2,8 @@ object Test: class User(val name: String, val age: Int, val city: String) - //TODO: Test this object UserEx: + type Reverse[A, B] = (B, A) class UserExtractor(user: User) extends Product: def _1: String = println("Got name") @@ -15,7 +15,7 @@ object Test: println("Got city") user.city - type Names = ("name", "age", "city") + type Names = "name" *: Reverse["city", "age"] // Members declared in scala.Equals def canEqual(that: Any): Boolean = ??? @@ -41,7 +41,7 @@ object Test: def main(args: Array[String]): Unit = val UserEx(city = c, name = _) = User("Guy", 25, "Paris") - println("city = " + c: String) + println("city = " + c) val UserEx2(city = _, name = n) = User("Guy", 25, "Paris") println(n) From 43d20822d36a2aca859b1086a01bc3d72951f383 Mon Sep 17 00:00:00 2001 From: Jentsch Date: Sat, 11 Jun 2022 23:07:54 +0200 Subject: [PATCH 22/26] Remove regex hack --- compiler/src/dotty/tools/dotc/parsing/Parsers.scala | 2 +- .../src/dotty/tools/dotc/typer/Applications.scala | 13 +++++++++---- tests/pos/namedPatternMatching.scala | 4 ++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 2ddd58636347..671e5cfe9ce3 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -2838,7 +2838,7 @@ object Parsers { */ def namedPattern(location: Location = Location.InPattern): Tree = // TODO: Figure out the performance impact of this lookahead - if (in.token == IDENTIFIER && in.lookahead.token == EQUALS) + if ((in.token == IDENTIFIER || in.token == BACKQUOTED_IDENT) && in.lookahead.token == EQUALS) then val ident = termIdent() accept(EQUALS) NamedArg(ident.name, pattern(location)) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 2a7f2a846925..a56567f76cfa 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -1413,10 +1413,15 @@ trait Applications extends Compatibility { val positionOfStringNames: Map[String, Int] = if names.exists then - // TODO: Don't use regular expression to deconstruct tuples - val reg = "\"([^\"]+)\"".r - reg.findAllMatchIn(names.showDcl) - .map(_.group(1)) + // 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? diff --git a/tests/pos/namedPatternMatching.scala b/tests/pos/namedPatternMatching.scala index 961e1581f98a..cf2709182f84 100644 --- a/tests/pos/namedPatternMatching.scala +++ b/tests/pos/namedPatternMatching.scala @@ -10,7 +10,7 @@ object Age: Some(age.hidden.asInstanceOf) object StringExample: - def unapply(str: String): Option[(Char, Char) & { type Names = "first" *: "last" *: EmptyTuple }] = + 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) @@ -27,7 +27,7 @@ val User(name = name, age = Age(years = years)) = user // partial function val maybeTom = Some(user).collect { - case u @ User(name = StringExample(last = 'm')) => u + case u @ User(name = StringExample(`"` = 'm')) => u } val berlinerNames = for From 2c4b58cd218a0465a06dc07532c2a5fd9b6cb5be Mon Sep 17 00:00:00 2001 From: Jentsch Date: Sat, 11 Jun 2022 23:09:05 +0200 Subject: [PATCH 23/26] Add todo --- compiler/src/dotty/tools/dotc/parsing/Parsers.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 671e5cfe9ce3..6686b60c0992 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -2831,6 +2831,7 @@ object Parsers { /** 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(() => namedPattern(location)) From 4312f86437a34f69c84f7ba78c579b1fa7e3568d Mon Sep 17 00:00:00 2001 From: Jentsch Date: Sun, 12 Jun 2022 06:41:46 +0200 Subject: [PATCH 24/26] Remove casting --- compiler/src/dotty/tools/dotc/typer/Applications.scala | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index a56567f76cfa..7b3b65582d66 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -1401,11 +1401,9 @@ trait Applications extends Compatibility { // TODO: Maybe the 'reorder' method above can be reused, or be template if (bunchedArgs != Nil && remainingArgTypes != Nil) { - val resTypeOfUnapplyFn = unapplyFn.tpe.widen.asInstanceOf[MethodType].resType - val names: SingleDenotation = - resTypeOfUnapplyFn.member(nme.get).info - .orElse(resTypeOfUnapplyFn) + 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? From c0ccb6f0d28afeea70c91a3c215dd6fe2eccea5e Mon Sep 17 00:00:00 2001 From: Jentsch Date: Sun, 12 Jun 2022 07:45:09 +0200 Subject: [PATCH 25/26] Document minor bug --- tests/neg/negNamedPatternMatching.check | 10 ++++++++++ tests/neg/negNamedPatternMatching.scala | 5 ++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/neg/negNamedPatternMatching.check b/tests/neg/negNamedPatternMatching.check index 9f5e61ef47c6..b9256ea4acbc 100644 --- a/tests/neg/negNamedPatternMatching.check +++ b/tests/neg/negNamedPatternMatching.check @@ -26,3 +26,13 @@ 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` diff --git a/tests/neg/negNamedPatternMatching.scala b/tests/neg/negNamedPatternMatching.scala index cca15b248ab1..eb37f5958a30 100644 --- a/tests/neg/negNamedPatternMatching.scala +++ b/tests/neg/negNamedPatternMatching.scala @@ -49,4 +49,7 @@ val annasCity = user match name = "Tom 3" // error ) => null case User(_, name = "Anna") => null // error - case User(city = City(name = "Berlin")) => null // error \ No newline at end of file + case User(city = City(name = "Berlin")) => null // error + +// TODO: Don't show an error about recursive value +val User(names = notRecursive) = user // error // error From 01c79b1ca942550ac865cd3cdf267c2c7ec93b79 Mon Sep 17 00:00:00 2001 From: Jentsch Date: Sun, 10 Jul 2022 23:03:38 +0200 Subject: [PATCH 26/26] Deny named arguments in vararg patterns --- compiler/src/dotty/tools/dotc/typer/Applications.scala | 5 ++++- tests/neg/negNamedPatternMatching.check | 4 ++++ tests/neg/negNamedPatternMatching.scala | 10 ++++++++++ tests/run/runNamedPatternMatching.check | 4 ---- tests/run/runNamedPatternMatching.scala | 7 ++++--- 5 files changed, 22 insertions(+), 8 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 944dca8ea51f..22e333edb2c1 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -1410,7 +1410,10 @@ trait Applications extends Compatibility { .asSingleDenotation val positionOfStringNames: Map[String, Int] = - if names.exists then + 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 diff --git a/tests/neg/negNamedPatternMatching.check b/tests/neg/negNamedPatternMatching.check index b9256ea4acbc..00c621563afa 100644 --- a/tests/neg/negNamedPatternMatching.check +++ b/tests/neg/negNamedPatternMatching.check @@ -36,3 +36,7 @@ | 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 diff --git a/tests/neg/negNamedPatternMatching.scala b/tests/neg/negNamedPatternMatching.scala index eb37f5958a30..ee0610b1c119 100644 --- a/tests/neg/negNamedPatternMatching.scala +++ b/tests/neg/negNamedPatternMatching.scala @@ -53,3 +53,13 @@ val annasCity = user match // 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") +} diff --git a/tests/run/runNamedPatternMatching.check b/tests/run/runNamedPatternMatching.check index f12af34956da..a3fd5f183584 100644 --- a/tests/run/runNamedPatternMatching.check +++ b/tests/run/runNamedPatternMatching.check @@ -1,5 +1 @@ -Got name -Got age -Got city -city = Paris Guy diff --git a/tests/run/runNamedPatternMatching.scala b/tests/run/runNamedPatternMatching.scala index a1def4d3f92f..6c6cc629a449 100644 --- a/tests/run/runNamedPatternMatching.scala +++ b/tests/run/runNamedPatternMatching.scala @@ -30,7 +30,7 @@ object Test: val empty: EmptyTuple & { type Names = EmptyTuple } = EmptyTuple.asInstanceOf extension [N <: Singleton & String, V] (head: (N, V)) - def +::[Vs <: Tuple, Ns <: Tuple](tail: Vs & { type Names = Ns }) : V *: Vs & { type Names = N *: Ns }= + def +::[Vs <: Tuple, Ns <: Tuple](tail: Vs & { type Names = Ns }) : V *: Vs & { type Names = N *: Ns } = (head._2 *: tail).asInstanceOf object UserEx2: @@ -40,8 +40,9 @@ object Test: ("name", user.name) +:: ("age", user.age) +:: ("city", user.city) +:: empty def main(args: Array[String]): Unit = - val UserEx(city = c, name = _) = User("Guy", 25, "Paris") - println("city = " + c) + // TODO: following line leads to "Recursive value c needs type" + // val UserEx(city = c, name = _) = User("Guy", 25, "Paris") + // println("city = " + c) val UserEx2(city = _, name = n) = User("Guy", 25, "Paris") println(n)