From 3478f202a18b49633fed673faa5f362079fb7e8f Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Tue, 19 Jun 2018 17:16:20 +0200 Subject: [PATCH 1/5] Add type refinement for abstract type bindings --- .../src/dotty/tools/dotc/core/StdNames.scala | 1 + .../tools/dotc/transform/FirstTransform.scala | 18 +++ .../dotty/tools/dotc/typer/Applications.scala | 2 +- .../src/dotty/tools/dotc/typer/Typer.scala | 23 +++- tests/neg/refined-binding-nat.scala | 55 ++++++++ tests/run/refined-binding-nat.check | 6 + tests/run/refined-binding-nat.scala | 121 ++++++++++++++++++ tests/run/refined-binding.check | 2 + tests/run/refined-binding.scala | 38 ++++++ 9 files changed, 263 insertions(+), 3 deletions(-) create mode 100644 tests/neg/refined-binding-nat.scala create mode 100644 tests/run/refined-binding-nat.check create mode 100644 tests/run/refined-binding-nat.scala create mode 100644 tests/run/refined-binding.check create mode 100644 tests/run/refined-binding.scala diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 03c3f49ff94a..cbb38f00fcad 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -489,6 +489,7 @@ object StdNames { val productPrefix: N = "productPrefix" val raw_ : N = "raw" val readResolve: N = "readResolve" + val refinedScrutinee: N = "refinedScrutinee" val reflect : N = "reflect" val reflectiveSelectable: N = "reflectiveSelectable" val reify : N = "reify" diff --git a/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala b/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala index 27eabbaea25d..4392734653dd 100644 --- a/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala +++ b/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala @@ -109,6 +109,24 @@ class FirstTransform extends MiniPhase with InfoTransformer { thisPhase => } override def transformDefDef(ddef: DefDef)(implicit ctx: Context) = { + if (ddef.name == nme.unapply && !ddef.symbol.is(Synthetic)) { + ddef.tpe.widen match { + case mt: MethodType if !mt.resType.widen.isInstanceOf[MethodicType] => + val resultType = mt.resType.substParam(mt.paramRefs.head, mt.paramRefs.head) + val refinedType = resultType.select(nme.refinedScrutinee).widen.resultType + if (refinedType.exists && !(refinedType <:< mt.paramRefs.head)) { + val paramName = mt.paramNames.head + val paramTpe = mt.paramRefs.head + val paramInfo = mt.paramInfos.head + ctx.error( + i"""Extractor with ${nme.refinedScrutinee} should refine the result type of that member. + |The result type of ${nme.refinedScrutinee} should be a subtype of $paramTpe: + | def unapply($paramName: $paramInfo): ${resultType.widenDealias.classSymbol.name} { def ${nme.refinedScrutinee}: $refinedType & $paramTpe } + """.stripMargin, ddef.tpt.pos) + } + case _ => + } + } val meth = ddef.symbol.asTerm if (meth.hasAnnotation(defn.NativeAnnot)) { meth.resetFlag(Deferred) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index bb2f292cf772..5b84e2c864cc 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -939,7 +939,7 @@ trait Applications extends Compatibility { self: Typer with Dynamic => * whereas overloaded variants need to have a conforming variant. */ def trySelectUnapply(qual: untpd.Tree)(fallBack: Tree => Tree): Tree = { - // try first for non-overloaded, then for overloaded ocurrences + // try first for non-overloaded, then for overloaded occurrences def tryWithName(name: TermName)(fallBack: Tree => Tree)(implicit ctx: Context): Tree = { def tryWithProto(pt: Type)(implicit ctx: Context) = { val result = typedExpr(untpd.Select(qual, name), new UnapplyFunProto(pt, this)) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 6f7dde5baa70..f206e453b026 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1371,10 +1371,29 @@ class Typer extends Namer else { // for a singleton pattern like `x @ Nil`, `x` should get the type from the scrutinee // see tests/neg/i3200b.scala and SI-1503 - val symTp = + val symTp0 = if (body1.tpe.isInstanceOf[TermRef]) pt1 else body1.tpe.underlyingIfRepeated(isJava = false) - val sym = ctx.newPatternBoundSymbol(tree.name, symTp, tree.pos) + + // If it is name based pattern matching, the type of the argument of the unapply is abstract and + // the return type has a type member `Refined`, then refine the type of the binding with the type of `Refined`. + val symTp1 = body1 match { + case Trees.UnApply(fun, _, _) if symTp0.typeSymbol.is(Deferred) => + // TODO check that it is name based pattern matching + fun.tpe.widen match { + case mt: MethodType if !mt.resType.isInstanceOf[MethodType] => + val resultType = mt.resType.substParam(mt.paramRefs.head, symTp0) + val refinedType = resultType.select(nme.refinedScrutinee).widen.resultType + if (refinedType.exists) refinedType + else symTp0 + case _ => + symTp0 + } + + case _ => symTp0 + } + + val sym = ctx.newPatternBoundSymbol(tree.name, symTp1, tree.pos) if (ctx.mode.is(Mode.InPatternAlternative)) ctx.error(i"Illegal variable ${sym.name} in pattern alternative", tree.pos) assignType(cpy.Bind(tree)(tree.name, body1), sym) diff --git a/tests/neg/refined-binding-nat.scala b/tests/neg/refined-binding-nat.scala new file mode 100644 index 000000000000..510e0e8efddd --- /dev/null +++ b/tests/neg/refined-binding-nat.scala @@ -0,0 +1,55 @@ + +trait Peano { + type Nat + type Zero <: Nat + type Succ <: Nat + + val Zero: Zero + + val Succ: SuccExtractor + trait SuccExtractor { + def apply(nat: Nat): Succ + def unapply(nat: Nat): SuccOpt // error: missing { def refinedScrutinee: Succ & nat.type } + } + trait SuccOpt { + def isEmpty: Boolean + def refinedScrutinee: Succ + def get: Nat + } +} + +object IntNums extends Peano { + type Nat = Int + type Zero = Int + type Succ = Int + + val Zero: Zero = 0 + + object Succ extends SuccExtractor { + def apply(nat: Nat): Succ = nat + 1 + def unapply(nat: Nat) = new SuccOpt { // error: missing { def refinedScrutinee: Succ & nat.type } + def isEmpty: Boolean = nat == 0 + def refinedScrutinee: Succ & nat.type = nat + def get: Int = nat - 1 + } + } + +} + +object IntNums2 extends Peano { + type Nat = Int + type Zero = Int + type Succ = Int + + val Zero: Zero = 0 + + object Succ extends SuccExtractor { + def apply(nat: Nat): Succ = nat + 1 + def unapply(nat: Nat): SuccOpt { def refinedScrutinee: Succ & nat.type } = new SuccOpt { + def isEmpty: Boolean = nat == 0 + def refinedScrutinee: Succ & nat.type = nat + def get: Int = nat - 1 + } + } + +} \ No newline at end of file diff --git a/tests/run/refined-binding-nat.check b/tests/run/refined-binding-nat.check new file mode 100644 index 000000000000..5dc5e167eeae --- /dev/null +++ b/tests/run/refined-binding-nat.check @@ -0,0 +1,6 @@ +Some((SuccClass(SuccClass(ZeroObject)),SuccClass(ZeroObject))) +Some((ZeroObject,SuccClass(SuccClass(ZeroObject)))) +None +Some((2,1)) +Some((0,2)) +None diff --git a/tests/run/refined-binding-nat.scala b/tests/run/refined-binding-nat.scala new file mode 100644 index 000000000000..aceb82192900 --- /dev/null +++ b/tests/run/refined-binding-nat.scala @@ -0,0 +1,121 @@ + +object Test { + def main(args: Array[String]): Unit = { + app(ClassNums) + app(IntNums) + } + + def app(peano: Peano): Unit = { + import peano._ + def divOpt(m: Nat, n: Nat): Option[(Nat, Nat)] = { + n match { + case Zero => None + case s @ Succ(_) => Some(safeDiv(m, s)) + } + } + val two = Succ(Succ(Zero)) + val five = Succ(Succ(Succ(two))) + println(divOpt(five, two)) + println(divOpt(two, five)) + println(divOpt(two, Zero)) + } +} + +trait Peano { + type Nat + type Zero <: Nat + type Succ <: Nat + + def safeDiv(m: Nat, n: Succ): (Nat, Nat) + + implicit def succDeco(succ: Succ): SuccAPI + trait SuccAPI { + def pred: Nat + } + + val Zero: Zero + + val Succ: SuccExtractor + trait SuccExtractor { + def apply(nat: Nat): Succ + def unapply(nat: Nat): SuccOpt { def refinedScrutinee: Succ & nat.type } + } + trait SuccOpt { + def isEmpty: Boolean + def refinedScrutinee: Succ + def get: Nat + } +} + +object IntNums extends Peano { + type Nat = Int + type Zero = Int + type Succ = Int + + def safeDiv(m: Nat, n: Succ): (Nat, Nat) = (m / n, m % n) + + val Zero: Zero = 0 + + object Succ extends SuccExtractor { + def apply(nat: Nat): Succ = nat + 1 + def unapply(nat: Nat) = new SuccOpt { + def isEmpty: Boolean = nat == 0 + def refinedScrutinee: Succ & nat.type = nat + def get: Int = nat - 1 + } + } + + def succDeco(succ: Succ): SuccAPI = new SuccAPI { + def pred: Nat = succ - 1 + } +} + +object ClassNums extends Peano { + trait NatTrait + object ZeroObject extends NatTrait { + override def toString: String = "ZeroObject" + } + case class SuccClass(predecessor: NatTrait) extends NatTrait with SuccOpt { + def isEmpty: Boolean = false + def refinedScrutinee: this.type = this + def get: NatTrait = this + } + + object SuccNoMatch extends SuccOpt { + def isEmpty: Boolean = true + def refinedScrutinee: Nothing = throw new NoSuchElementException + def get: NatTrait = throw new NoSuchElementException + } + + type Nat = NatTrait + type Zero = ZeroObject.type + type Succ = SuccClass + + def safeDiv(m: Nat, n: Succ): (Nat, Nat) = { + def intValue(x: Nat, acc: Int): Int = x match { + case nat: SuccClass => intValue(nat.predecessor, acc + 1) + case _ => acc + } + def natValue(x: Int): Nat = + if (x == 0) ZeroObject + else new SuccClass(natValue(x - 1)) + val i = intValue(m, 0) + val j = intValue(n, 0) + (natValue(i / j), natValue(i % j)) + } + + val Zero: Zero = ZeroObject + + object Succ extends SuccExtractor { + def apply(nat: Nat): Succ = new SuccClass(nat) + def unapply(nat: Nat) = nat match { + case nat: (SuccClass & nat.type) => nat + case _ => SuccNoMatch + } + } + + def succDeco(succ: Succ): SuccAPI = new SuccAPI { + def pred: Nat = succ.predecessor + } + +} diff --git a/tests/run/refined-binding.check b/tests/run/refined-binding.check new file mode 100644 index 000000000000..14e859cfe54b --- /dev/null +++ b/tests/run/refined-binding.check @@ -0,0 +1,2 @@ +ok +9 diff --git a/tests/run/refined-binding.scala b/tests/run/refined-binding.scala new file mode 100644 index 000000000000..d5be1657f6ec --- /dev/null +++ b/tests/run/refined-binding.scala @@ -0,0 +1,38 @@ + +sealed trait Foo { + + type X + type Y <: X + + def x: X + + def f(y: Y) = println("ok") + + object Z { + def unapply(arg: X) = new Opt { + type Scrutinee = arg.type + def refinedScrutinee: Y & Scrutinee = arg.asInstanceOf[Y & Scrutinee] + } + } + + abstract class Opt { + type Scrutinee <: Singleton + def refinedScrutinee: Y & Scrutinee + def get: Int = 9 + def isEmpty: Boolean = false + } +} + +object Test { + def main(args: Array[String]): Unit = { + test(new Foo { type X = Int; type Y = Int; def x: X = 1 }) + } + + def test(foo: Foo): Unit = { + foo.x match { + case x @ foo.Z(i) => // `x` is refined to type `Y` + foo.f(x) + println(i) + } + } +} From 06671cf47e9064e126c8f516d43e7ae4493a0057 Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Mon, 13 Aug 2018 11:51:10 +0200 Subject: [PATCH 2/5] WIP add test for different refinement approach --- .../dotty/tools/dotc/core/Definitions.scala | 3 ++ .../src/dotty/tools/dotc/core/StdNames.scala | 3 +- .../tools/dotc/transform/FirstTransform.scala | 22 +++++++------ .../src/dotty/tools/dotc/typer/Typer.scala | 8 +++-- library/src/scala/RefinedScrutinee.scala | 17 ++++++++++ tests/neg/refined-binding-nat.scala | 23 ++++--------- tests/run/refined-binding-nat.scala | 32 ++++--------------- tests/run/refined-binding.scala | 13 ++------ 8 files changed, 56 insertions(+), 65 deletions(-) create mode 100644 library/src/scala/RefinedScrutinee.scala diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index d43611c53f52..aace78783444 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -701,6 +701,9 @@ class Definitions { lazy val TastyTastyModule = ctx.requiredModule("scala.tasty.Tasty") lazy val TastyTasty_macroContext = TastyTastyModule.requiredMethod("macroContext") + lazy val RefinedScrutineeType: TypeRef = ctx.requiredClassRef("scala.RefinedScrutinee") + def RefinedScrutineeClass(implicit ctx: Context) = RefinedScrutineeType.symbol.asClass + lazy val EqType = ctx.requiredClassRef("scala.Eq") def EqClass(implicit ctx: Context) = EqType.symbol.asClass def EqModule(implicit ctx: Context) = EqClass.companionModule diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index cbb38f00fcad..72223dfa6ea9 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -217,6 +217,8 @@ object StdNames { final val Type : N = "Type" final val TypeTree: N = "TypeTree" + final val RefinedScrutinee: N = "RefinedScrutinee" + // Annotation simple names, used in Namer final val BeanPropertyAnnot: N = "BeanProperty" final val BooleanBeanPropertyAnnot: N = "BooleanBeanProperty" @@ -489,7 +491,6 @@ object StdNames { val productPrefix: N = "productPrefix" val raw_ : N = "raw" val readResolve: N = "readResolve" - val refinedScrutinee: N = "refinedScrutinee" val reflect : N = "reflect" val reflectiveSelectable: N = "reflectiveSelectable" val reify : N = "reify" diff --git a/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala b/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala index 4392734653dd..369ac2e05590 100644 --- a/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala +++ b/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala @@ -113,16 +113,20 @@ class FirstTransform extends MiniPhase with InfoTransformer { thisPhase => ddef.tpe.widen match { case mt: MethodType if !mt.resType.widen.isInstanceOf[MethodicType] => val resultType = mt.resType.substParam(mt.paramRefs.head, mt.paramRefs.head) - val refinedType = resultType.select(nme.refinedScrutinee).widen.resultType - if (refinedType.exists && !(refinedType <:< mt.paramRefs.head)) { - val paramName = mt.paramNames.head - val paramTpe = mt.paramRefs.head - val paramInfo = mt.paramInfos.head - ctx.error( - i"""Extractor with ${nme.refinedScrutinee} should refine the result type of that member. - |The result type of ${nme.refinedScrutinee} should be a subtype of $paramTpe: - | def unapply($paramName: $paramInfo): ${resultType.widenDealias.classSymbol.name} { def ${nme.refinedScrutinee}: $refinedType & $paramTpe } + resultType match { + case resultType: AppliedType if resultType.derivesFrom(defn.RefinedScrutineeClass) => + val refinedType :: resultType2 :: Nil = resultType.args + if (refinedType.exists && !(refinedType <:< mt.paramRefs.head)) { + val paramName = mt.paramNames.head + val paramTpe = mt.paramRefs.head + val paramInfo = mt.paramInfos.head + ctx.error( + i"""Extractor with ${tpnme.RefinedScrutinee} should refine the result type of that member. + |The scrutinee type of ${tpnme.RefinedScrutinee} should be a subtype of $paramTpe: + | def unapply($paramName: $paramInfo): ${tpnme.RefinedScrutinee}[$paramTpe & $refinedType, $resultType2] """.stripMargin, ddef.tpt.pos) + } + case _ => } case _ => } diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index f206e453b026..5e0c888d72ad 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1383,9 +1383,11 @@ class Typer extends Namer fun.tpe.widen match { case mt: MethodType if !mt.resType.isInstanceOf[MethodType] => val resultType = mt.resType.substParam(mt.paramRefs.head, symTp0) - val refinedType = resultType.select(nme.refinedScrutinee).widen.resultType - if (refinedType.exists) refinedType - else symTp0 + resultType match { + case resultType: AppliedType if resultType.derivesFrom(defn.RefinedScrutineeClass) => + resultType.args.head + case _ => symTp0 + } case _ => symTp0 } diff --git a/library/src/scala/RefinedScrutinee.scala b/library/src/scala/RefinedScrutinee.scala new file mode 100644 index 000000000000..346ccefa1a2a --- /dev/null +++ b/library/src/scala/RefinedScrutinee.scala @@ -0,0 +1,17 @@ +package scala + +class RefinedScrutinee[+Scrutinee /*<: Singleton*/, +Result] private (val result: Option[Result]) extends AnyVal { + // Scrutinee in not a singleton to provide a better error message + + /** There is no result */ + def isEmpty: Boolean = result.isEmpty + + /** Get the result */ + def get: Result = result.get +} + +object RefinedScrutinee { + // TODO when bootstrapped: erase scrutinee as it is just an evidence for that existance of a term of type Scrutinee + def matchOf[Scrutinee <: Singleton, Result](/*erased*/ scrutinee: Scrutinee)(result: Result): RefinedScrutinee[Scrutinee, Result] = new RefinedScrutinee(Some(result)) + def noMatch[Scrutinee <: Singleton, Result]: RefinedScrutinee[Scrutinee, Result] = new RefinedScrutinee(None) +} diff --git a/tests/neg/refined-binding-nat.scala b/tests/neg/refined-binding-nat.scala index 510e0e8efddd..604b143bdb82 100644 --- a/tests/neg/refined-binding-nat.scala +++ b/tests/neg/refined-binding-nat.scala @@ -9,12 +9,7 @@ trait Peano { val Succ: SuccExtractor trait SuccExtractor { def apply(nat: Nat): Succ - def unapply(nat: Nat): SuccOpt // error: missing { def refinedScrutinee: Succ & nat.type } - } - trait SuccOpt { - def isEmpty: Boolean - def refinedScrutinee: Succ - def get: Nat + def unapply(nat: Nat): RefinedScrutinee[Succ, Nat] // error } } @@ -27,11 +22,9 @@ object IntNums extends Peano { object Succ extends SuccExtractor { def apply(nat: Nat): Succ = nat + 1 - def unapply(nat: Nat) = new SuccOpt { // error: missing { def refinedScrutinee: Succ & nat.type } - def isEmpty: Boolean = nat == 0 - def refinedScrutinee: Succ & nat.type = nat - def get: Int = nat - 1 - } + def unapply(nat: Nat) = // error + if (nat == 0) RefinedScrutinee.noMatch + else RefinedScrutinee.matchOf(nat)(nat - 1) } } @@ -45,11 +38,9 @@ object IntNums2 extends Peano { object Succ extends SuccExtractor { def apply(nat: Nat): Succ = nat + 1 - def unapply(nat: Nat): SuccOpt { def refinedScrutinee: Succ & nat.type } = new SuccOpt { - def isEmpty: Boolean = nat == 0 - def refinedScrutinee: Succ & nat.type = nat - def get: Int = nat - 1 - } + def unapply(nat: Nat): RefinedScrutinee[nat.type & Succ, Nat] = + if (nat == 0) RefinedScrutinee.noMatch + else RefinedScrutinee.matchOf(nat)(nat - 1) } } \ No newline at end of file diff --git a/tests/run/refined-binding-nat.scala b/tests/run/refined-binding-nat.scala index aceb82192900..96470aa8018b 100644 --- a/tests/run/refined-binding-nat.scala +++ b/tests/run/refined-binding-nat.scala @@ -38,12 +38,7 @@ trait Peano { val Succ: SuccExtractor trait SuccExtractor { def apply(nat: Nat): Succ - def unapply(nat: Nat): SuccOpt { def refinedScrutinee: Succ & nat.type } - } - trait SuccOpt { - def isEmpty: Boolean - def refinedScrutinee: Succ - def get: Nat + def unapply(nat: Nat): RefinedScrutinee[nat.type & Succ, Nat] } } @@ -58,13 +53,10 @@ object IntNums extends Peano { object Succ extends SuccExtractor { def apply(nat: Nat): Succ = nat + 1 - def unapply(nat: Nat) = new SuccOpt { - def isEmpty: Boolean = nat == 0 - def refinedScrutinee: Succ & nat.type = nat - def get: Int = nat - 1 - } + def unapply(nat: Nat) = + if (nat == 0) RefinedScrutinee.noMatch + else RefinedScrutinee.matchOf(nat)(nat - 1) } - def succDeco(succ: Succ): SuccAPI = new SuccAPI { def pred: Nat = succ - 1 } @@ -75,17 +67,7 @@ object ClassNums extends Peano { object ZeroObject extends NatTrait { override def toString: String = "ZeroObject" } - case class SuccClass(predecessor: NatTrait) extends NatTrait with SuccOpt { - def isEmpty: Boolean = false - def refinedScrutinee: this.type = this - def get: NatTrait = this - } - - object SuccNoMatch extends SuccOpt { - def isEmpty: Boolean = true - def refinedScrutinee: Nothing = throw new NoSuchElementException - def get: NatTrait = throw new NoSuchElementException - } + case class SuccClass(predecessor: NatTrait) extends NatTrait type Nat = NatTrait type Zero = ZeroObject.type @@ -109,8 +91,8 @@ object ClassNums extends Peano { object Succ extends SuccExtractor { def apply(nat: Nat): Succ = new SuccClass(nat) def unapply(nat: Nat) = nat match { - case nat: (SuccClass & nat.type) => nat - case _ => SuccNoMatch + case nat: (SuccClass & nat.type) => RefinedScrutinee.matchOf(nat)(nat.predecessor) + case _ => RefinedScrutinee.noMatch } } diff --git a/tests/run/refined-binding.scala b/tests/run/refined-binding.scala index d5be1657f6ec..258a635e62d4 100644 --- a/tests/run/refined-binding.scala +++ b/tests/run/refined-binding.scala @@ -9,17 +9,8 @@ sealed trait Foo { def f(y: Y) = println("ok") object Z { - def unapply(arg: X) = new Opt { - type Scrutinee = arg.type - def refinedScrutinee: Y & Scrutinee = arg.asInstanceOf[Y & Scrutinee] - } - } - - abstract class Opt { - type Scrutinee <: Singleton - def refinedScrutinee: Y & Scrutinee - def get: Int = 9 - def isEmpty: Boolean = false + def unapply(arg: X): RefinedScrutinee[arg.type & Y, Int] = + RefinedScrutinee.matchOf(arg.asInstanceOf[arg.type & Y])(9) } } From 18f672b3f129aebf69061d55cb1704210e98b576 Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Mon, 13 Aug 2018 18:06:46 +0200 Subject: [PATCH 3/5] WIP non boxing --- library/src/scala/RefinedScrutinee.scala | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/library/src/scala/RefinedScrutinee.scala b/library/src/scala/RefinedScrutinee.scala index 346ccefa1a2a..0a7d4aed567a 100644 --- a/library/src/scala/RefinedScrutinee.scala +++ b/library/src/scala/RefinedScrutinee.scala @@ -1,17 +1,18 @@ package scala -class RefinedScrutinee[+Scrutinee /*<: Singleton*/, +Result] private (val result: Option[Result]) extends AnyVal { +final class RefinedScrutinee[+Scrutinee /*<: Singleton*/, +Result] private (val result: Any) extends AnyVal { // Scrutinee in not a singleton to provide a better error message /** There is no result */ - def isEmpty: Boolean = result.isEmpty + def isEmpty: Boolean = result == RefinedScrutinee.NoResult /** Get the result */ - def get: Result = result.get + def get: Result = result.asInstanceOf[Result] } object RefinedScrutinee { - // TODO when bootstrapped: erase scrutinee as it is just an evidence for that existance of a term of type Scrutinee - def matchOf[Scrutinee <: Singleton, Result](/*erased*/ scrutinee: Scrutinee)(result: Result): RefinedScrutinee[Scrutinee, Result] = new RefinedScrutinee(Some(result)) - def noMatch[Scrutinee <: Singleton, Result]: RefinedScrutinee[Scrutinee, Result] = new RefinedScrutinee(None) + private[RefinedScrutinee] object NoResult + + def matchOf[Scrutinee <: Singleton, Result](scrutinee: Scrutinee)(result: Result): RefinedScrutinee[Scrutinee, Result] = new RefinedScrutinee(result) + def noMatch[Scrutinee <: Singleton, Result]: RefinedScrutinee[Scrutinee, Result] = new RefinedScrutinee(NoResult) } From 11b9df688d514f64c22845162911ea6873516bd6 Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Tue, 14 Aug 2018 09:01:21 +0200 Subject: [PATCH 4/5] Implement RefinedScrutinee with def refinedScrutinee --- .../dotty/tools/dotc/core/Definitions.scala | 2 +- .../src/dotty/tools/dotc/core/StdNames.scala | 1 + .../tools/dotc/transform/FirstTransform.scala | 11 ++ .../src/dotty/tools/dotc/typer/Typer.scala | 8 +- library/src/scala/RefinedScrutinee.scala | 9 +- tests/neg/refined-binding-nat-2.scala | 46 ++++++++ tests/neg/refined-binding-nat.scala | 25 +++-- tests/run/refined-binding-2.check | 2 + tests/run/refined-binding-2.scala | 29 +++++ tests/run/refined-binding-nat-2.check | 6 + tests/run/refined-binding-nat-2.scala | 103 ++++++++++++++++++ tests/run/refined-binding-nat.scala | 32 ++++-- tests/run/refined-binding.scala | 13 ++- 13 files changed, 263 insertions(+), 24 deletions(-) create mode 100644 tests/neg/refined-binding-nat-2.scala create mode 100644 tests/run/refined-binding-2.check create mode 100644 tests/run/refined-binding-2.scala create mode 100644 tests/run/refined-binding-nat-2.check create mode 100644 tests/run/refined-binding-nat-2.scala diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index aace78783444..92c65558352f 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -701,7 +701,7 @@ class Definitions { lazy val TastyTastyModule = ctx.requiredModule("scala.tasty.Tasty") lazy val TastyTasty_macroContext = TastyTastyModule.requiredMethod("macroContext") - lazy val RefinedScrutineeType: TypeRef = ctx.requiredClassRef("scala.RefinedScrutinee") + lazy val RefinedScrutineeType: TypeRef = ctx.requiredClassRef("scala.RefinedScrutinee") def RefinedScrutineeClass(implicit ctx: Context) = RefinedScrutineeType.symbol.asClass lazy val EqType = ctx.requiredClassRef("scala.Eq") diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 72223dfa6ea9..a1bac7e04765 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -492,6 +492,7 @@ object StdNames { val raw_ : N = "raw" val readResolve: N = "readResolve" val reflect : N = "reflect" + val refinedScrutinee: N = "refinedScrutinee" val reflectiveSelectable: N = "reflectiveSelectable" val reify : N = "reify" val rootMirror : N = "rootMirror" diff --git a/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala b/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala index 369ac2e05590..21aebc87c5d2 100644 --- a/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala +++ b/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala @@ -127,6 +127,17 @@ class FirstTransform extends MiniPhase with InfoTransformer { thisPhase => """.stripMargin, ddef.tpt.pos) } case _ => + val refinedType = resultType.select(nme.refinedScrutinee).widen.resultType + if (refinedType.exists && !(refinedType <:< mt.paramRefs.head)) { + val paramName = mt.paramNames.head + val paramTpe = mt.paramRefs.head + val paramInfo = mt.paramInfos.head + ctx.error( + i"""Extractor with ${nme.refinedScrutinee} should refine the result type of that member. + |The result type of ${nme.refinedScrutinee} should be a subtype of $paramTpe: + | def unapply($paramName: $paramInfo): ${resultType.widenDealias.classSymbol.name} { def ${nme.refinedScrutinee}: $refinedType & $paramTpe } + """.stripMargin, ddef.tpt.pos) + } } case _ => } diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 5e0c888d72ad..f206e453b026 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1383,11 +1383,9 @@ class Typer extends Namer fun.tpe.widen match { case mt: MethodType if !mt.resType.isInstanceOf[MethodType] => val resultType = mt.resType.substParam(mt.paramRefs.head, symTp0) - resultType match { - case resultType: AppliedType if resultType.derivesFrom(defn.RefinedScrutineeClass) => - resultType.args.head - case _ => symTp0 - } + val refinedType = resultType.select(nme.refinedScrutinee).widen.resultType + if (refinedType.exists) refinedType + else symTp0 case _ => symTp0 } diff --git a/library/src/scala/RefinedScrutinee.scala b/library/src/scala/RefinedScrutinee.scala index 0a7d4aed567a..da9bfdb76994 100644 --- a/library/src/scala/RefinedScrutinee.scala +++ b/library/src/scala/RefinedScrutinee.scala @@ -6,13 +6,20 @@ final class RefinedScrutinee[+Scrutinee /*<: Singleton*/, +Result] private (val /** There is no result */ def isEmpty: Boolean = result == RefinedScrutinee.NoResult - /** Get the result */ + /** When non-empty, get the result */ def get: Result = result.asInstanceOf[Result] + + /** Scrutinee type on a successful match */ + /*erased*/ def refinedScrutinee: Scrutinee = null.asInstanceOf[Scrutinee] // evaluated in RefinedScrutinee.matchOf + } object RefinedScrutinee { + private[RefinedScrutinee] object NoResult def matchOf[Scrutinee <: Singleton, Result](scrutinee: Scrutinee)(result: Result): RefinedScrutinee[Scrutinee, Result] = new RefinedScrutinee(result) + def noMatch[Scrutinee <: Singleton, Result]: RefinedScrutinee[Scrutinee, Result] = new RefinedScrutinee(NoResult) + } diff --git a/tests/neg/refined-binding-nat-2.scala b/tests/neg/refined-binding-nat-2.scala new file mode 100644 index 000000000000..12ddba148e61 --- /dev/null +++ b/tests/neg/refined-binding-nat-2.scala @@ -0,0 +1,46 @@ + +trait Peano { + type Nat + type Zero <: Nat + type Succ <: Nat + + val Zero: Zero + + val Succ: SuccExtractor + trait SuccExtractor { + def apply(nat: Nat): Succ + def unapply(nat: Nat): RefinedScrutinee[Succ, Nat] // error: Extractor with RefinedScrutinee should refine the result type of that member ... + } +} + +object IntNums extends Peano { + type Nat = Int + type Zero = Int + type Succ = Int + + val Zero: Zero = 0 + + object Succ extends SuccExtractor { + def apply(nat: Nat): Succ = nat + 1 + def unapply(nat: Nat) = // error: Extractor with RefinedScrutinee should refine the result type of that member ... + if (nat == 0) RefinedScrutinee.noMatch + else RefinedScrutinee.matchOf(nat)(nat - 1) + } + +} + +object IntNums2 extends Peano { + type Nat = Int + type Zero = Int + type Succ = Int + + val Zero: Zero = 0 + + object Succ extends SuccExtractor { + def apply(nat: Nat): Succ = nat + 1 + def unapply(nat: Nat): RefinedScrutinee[nat.type & Succ, Nat] = + if (nat == 0) RefinedScrutinee.noMatch + else RefinedScrutinee.matchOf(nat)(nat - 1) + } + +} diff --git a/tests/neg/refined-binding-nat.scala b/tests/neg/refined-binding-nat.scala index 604b143bdb82..a1b8f653e960 100644 --- a/tests/neg/refined-binding-nat.scala +++ b/tests/neg/refined-binding-nat.scala @@ -9,7 +9,12 @@ trait Peano { val Succ: SuccExtractor trait SuccExtractor { def apply(nat: Nat): Succ - def unapply(nat: Nat): RefinedScrutinee[Succ, Nat] // error + def unapply(nat: Nat): SuccOpt // error: Extractor with refinedScrutinee should refine the result type of that member ... + } + trait SuccOpt { + def isEmpty: Boolean + def refinedScrutinee: Succ + def get: Nat } } @@ -22,9 +27,11 @@ object IntNums extends Peano { object Succ extends SuccExtractor { def apply(nat: Nat): Succ = nat + 1 - def unapply(nat: Nat) = // error - if (nat == 0) RefinedScrutinee.noMatch - else RefinedScrutinee.matchOf(nat)(nat - 1) + def unapply(nat: Nat) = new SuccOpt { // error: Extractor with refinedScrutinee should refine the result type of that member ... + def isEmpty: Boolean = nat == 0 + def refinedScrutinee: Succ & nat.type = nat + def get: Int = nat - 1 + } } } @@ -38,9 +45,11 @@ object IntNums2 extends Peano { object Succ extends SuccExtractor { def apply(nat: Nat): Succ = nat + 1 - def unapply(nat: Nat): RefinedScrutinee[nat.type & Succ, Nat] = - if (nat == 0) RefinedScrutinee.noMatch - else RefinedScrutinee.matchOf(nat)(nat - 1) + def unapply(nat: Nat): SuccOpt { def refinedScrutinee: Succ & nat.type } = new SuccOpt { + def isEmpty: Boolean = nat == 0 + def refinedScrutinee: Succ & nat.type = nat + def get: Int = nat - 1 + } } -} \ No newline at end of file +} diff --git a/tests/run/refined-binding-2.check b/tests/run/refined-binding-2.check new file mode 100644 index 000000000000..14e859cfe54b --- /dev/null +++ b/tests/run/refined-binding-2.check @@ -0,0 +1,2 @@ +ok +9 diff --git a/tests/run/refined-binding-2.scala b/tests/run/refined-binding-2.scala new file mode 100644 index 000000000000..258a635e62d4 --- /dev/null +++ b/tests/run/refined-binding-2.scala @@ -0,0 +1,29 @@ + +sealed trait Foo { + + type X + type Y <: X + + def x: X + + def f(y: Y) = println("ok") + + object Z { + def unapply(arg: X): RefinedScrutinee[arg.type & Y, Int] = + RefinedScrutinee.matchOf(arg.asInstanceOf[arg.type & Y])(9) + } +} + +object Test { + def main(args: Array[String]): Unit = { + test(new Foo { type X = Int; type Y = Int; def x: X = 1 }) + } + + def test(foo: Foo): Unit = { + foo.x match { + case x @ foo.Z(i) => // `x` is refined to type `Y` + foo.f(x) + println(i) + } + } +} diff --git a/tests/run/refined-binding-nat-2.check b/tests/run/refined-binding-nat-2.check new file mode 100644 index 000000000000..5dc5e167eeae --- /dev/null +++ b/tests/run/refined-binding-nat-2.check @@ -0,0 +1,6 @@ +Some((SuccClass(SuccClass(ZeroObject)),SuccClass(ZeroObject))) +Some((ZeroObject,SuccClass(SuccClass(ZeroObject)))) +None +Some((2,1)) +Some((0,2)) +None diff --git a/tests/run/refined-binding-nat-2.scala b/tests/run/refined-binding-nat-2.scala new file mode 100644 index 000000000000..96470aa8018b --- /dev/null +++ b/tests/run/refined-binding-nat-2.scala @@ -0,0 +1,103 @@ + +object Test { + def main(args: Array[String]): Unit = { + app(ClassNums) + app(IntNums) + } + + def app(peano: Peano): Unit = { + import peano._ + def divOpt(m: Nat, n: Nat): Option[(Nat, Nat)] = { + n match { + case Zero => None + case s @ Succ(_) => Some(safeDiv(m, s)) + } + } + val two = Succ(Succ(Zero)) + val five = Succ(Succ(Succ(two))) + println(divOpt(five, two)) + println(divOpt(two, five)) + println(divOpt(two, Zero)) + } +} + +trait Peano { + type Nat + type Zero <: Nat + type Succ <: Nat + + def safeDiv(m: Nat, n: Succ): (Nat, Nat) + + implicit def succDeco(succ: Succ): SuccAPI + trait SuccAPI { + def pred: Nat + } + + val Zero: Zero + + val Succ: SuccExtractor + trait SuccExtractor { + def apply(nat: Nat): Succ + def unapply(nat: Nat): RefinedScrutinee[nat.type & Succ, Nat] + } +} + +object IntNums extends Peano { + type Nat = Int + type Zero = Int + type Succ = Int + + def safeDiv(m: Nat, n: Succ): (Nat, Nat) = (m / n, m % n) + + val Zero: Zero = 0 + + object Succ extends SuccExtractor { + def apply(nat: Nat): Succ = nat + 1 + def unapply(nat: Nat) = + if (nat == 0) RefinedScrutinee.noMatch + else RefinedScrutinee.matchOf(nat)(nat - 1) + } + def succDeco(succ: Succ): SuccAPI = new SuccAPI { + def pred: Nat = succ - 1 + } +} + +object ClassNums extends Peano { + trait NatTrait + object ZeroObject extends NatTrait { + override def toString: String = "ZeroObject" + } + case class SuccClass(predecessor: NatTrait) extends NatTrait + + type Nat = NatTrait + type Zero = ZeroObject.type + type Succ = SuccClass + + def safeDiv(m: Nat, n: Succ): (Nat, Nat) = { + def intValue(x: Nat, acc: Int): Int = x match { + case nat: SuccClass => intValue(nat.predecessor, acc + 1) + case _ => acc + } + def natValue(x: Int): Nat = + if (x == 0) ZeroObject + else new SuccClass(natValue(x - 1)) + val i = intValue(m, 0) + val j = intValue(n, 0) + (natValue(i / j), natValue(i % j)) + } + + val Zero: Zero = ZeroObject + + object Succ extends SuccExtractor { + def apply(nat: Nat): Succ = new SuccClass(nat) + def unapply(nat: Nat) = nat match { + case nat: (SuccClass & nat.type) => RefinedScrutinee.matchOf(nat)(nat.predecessor) + case _ => RefinedScrutinee.noMatch + } + } + + def succDeco(succ: Succ): SuccAPI = new SuccAPI { + def pred: Nat = succ.predecessor + } + +} diff --git a/tests/run/refined-binding-nat.scala b/tests/run/refined-binding-nat.scala index 96470aa8018b..aceb82192900 100644 --- a/tests/run/refined-binding-nat.scala +++ b/tests/run/refined-binding-nat.scala @@ -38,7 +38,12 @@ trait Peano { val Succ: SuccExtractor trait SuccExtractor { def apply(nat: Nat): Succ - def unapply(nat: Nat): RefinedScrutinee[nat.type & Succ, Nat] + def unapply(nat: Nat): SuccOpt { def refinedScrutinee: Succ & nat.type } + } + trait SuccOpt { + def isEmpty: Boolean + def refinedScrutinee: Succ + def get: Nat } } @@ -53,10 +58,13 @@ object IntNums extends Peano { object Succ extends SuccExtractor { def apply(nat: Nat): Succ = nat + 1 - def unapply(nat: Nat) = - if (nat == 0) RefinedScrutinee.noMatch - else RefinedScrutinee.matchOf(nat)(nat - 1) + def unapply(nat: Nat) = new SuccOpt { + def isEmpty: Boolean = nat == 0 + def refinedScrutinee: Succ & nat.type = nat + def get: Int = nat - 1 + } } + def succDeco(succ: Succ): SuccAPI = new SuccAPI { def pred: Nat = succ - 1 } @@ -67,7 +75,17 @@ object ClassNums extends Peano { object ZeroObject extends NatTrait { override def toString: String = "ZeroObject" } - case class SuccClass(predecessor: NatTrait) extends NatTrait + case class SuccClass(predecessor: NatTrait) extends NatTrait with SuccOpt { + def isEmpty: Boolean = false + def refinedScrutinee: this.type = this + def get: NatTrait = this + } + + object SuccNoMatch extends SuccOpt { + def isEmpty: Boolean = true + def refinedScrutinee: Nothing = throw new NoSuchElementException + def get: NatTrait = throw new NoSuchElementException + } type Nat = NatTrait type Zero = ZeroObject.type @@ -91,8 +109,8 @@ object ClassNums extends Peano { object Succ extends SuccExtractor { def apply(nat: Nat): Succ = new SuccClass(nat) def unapply(nat: Nat) = nat match { - case nat: (SuccClass & nat.type) => RefinedScrutinee.matchOf(nat)(nat.predecessor) - case _ => RefinedScrutinee.noMatch + case nat: (SuccClass & nat.type) => nat + case _ => SuccNoMatch } } diff --git a/tests/run/refined-binding.scala b/tests/run/refined-binding.scala index 258a635e62d4..d5be1657f6ec 100644 --- a/tests/run/refined-binding.scala +++ b/tests/run/refined-binding.scala @@ -9,8 +9,17 @@ sealed trait Foo { def f(y: Y) = println("ok") object Z { - def unapply(arg: X): RefinedScrutinee[arg.type & Y, Int] = - RefinedScrutinee.matchOf(arg.asInstanceOf[arg.type & Y])(9) + def unapply(arg: X) = new Opt { + type Scrutinee = arg.type + def refinedScrutinee: Y & Scrutinee = arg.asInstanceOf[Y & Scrutinee] + } + } + + abstract class Opt { + type Scrutinee <: Singleton + def refinedScrutinee: Y & Scrutinee + def get: Int = 9 + def isEmpty: Boolean = false } } From 67bb6e621a33ffc83d72c3e807f435a324f9e4e5 Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Tue, 14 Aug 2018 09:30:20 +0200 Subject: [PATCH 5/5] WIP call refinedScrutinee --- tests/run/refined-binding-3.check | 3 +++ tests/run/refined-binding-3.scala | 41 +++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 tests/run/refined-binding-3.check create mode 100644 tests/run/refined-binding-3.scala diff --git a/tests/run/refined-binding-3.check b/tests/run/refined-binding-3.check new file mode 100644 index 000000000000..ea85545d4d31 --- /dev/null +++ b/tests/run/refined-binding-3.check @@ -0,0 +1,3 @@ +refinedScrutinee +ok +9 diff --git a/tests/run/refined-binding-3.scala b/tests/run/refined-binding-3.scala new file mode 100644 index 000000000000..49414a9cc632 --- /dev/null +++ b/tests/run/refined-binding-3.scala @@ -0,0 +1,41 @@ + +sealed trait Foo { + + type X + type Y <: X + + def x: X + + def f(y: Y) = println("ok") + + object Z { + def unapply(arg: X) = new Opt { + type Scrutinee = arg.type + def refinedScrutinee: Y & Scrutinee = { + println("refinedScrutinee") + arg.asInstanceOf[Y & Scrutinee] + } + } + } + + abstract class Opt { + type Scrutinee <: Singleton + def refinedScrutinee: Y & Scrutinee + def get: Int = 9 + def isEmpty: Boolean = false + } +} + +object Test { + def main(args: Array[String]): Unit = { + test(new Foo { type X = Int; type Y = Int; def x: X = 1 }) + } + + def test(foo: Foo): Unit = { + foo.x match { + case x @ foo.Z(i) => // `x` is refined to type `Y` + foo.f(x) + println(i) + } + } +}