From 29104c9755a9d6393959a416650422b84f0957f2 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sat, 31 Oct 2015 12:58:01 +0100 Subject: [PATCH 1/7] Auto-uncurry n-ary functions. Implements SIP #897. --- src/dotty/tools/dotc/ast/Desugar.scala | 19 +++++++ src/dotty/tools/dotc/typer/Typer.scala | 56 ++++++++++++------- test/dotc/tests.scala | 1 + tests/neg/function-arity.scala | 22 ++++++++ .../pos/{i873.scala => function-arity.scala} | 8 +++ 5 files changed, 87 insertions(+), 19 deletions(-) create mode 100644 tests/neg/function-arity.scala rename tests/pos/{i873.scala => function-arity.scala} (61%) diff --git a/src/dotty/tools/dotc/ast/Desugar.scala b/src/dotty/tools/dotc/ast/Desugar.scala index 87694843ad6a..c1083d26d283 100644 --- a/src/dotty/tools/dotc/ast/Desugar.scala +++ b/src/dotty/tools/dotc/ast/Desugar.scala @@ -588,6 +588,25 @@ object desugar { Function(params, Match(selector, cases)) } + /** Map n-ary function `(p1, ..., pn) => body` where n != 1 to unary function as follows: + * + * x$1 => { + * val p1 = x$1._1 + * ... + * val pn = x$1._n + * body + * } + */ + def makeUnaryCaseLambda(params: List[ValDef], body: Tree)(implicit ctx: Context): Tree = { + val param = makeSyntheticParameter() + def selector(n: Int) = Select(refOfDef(param), nme.selectorName(n)) + val vdefs = + params.zipWithIndex.map{ + case(param, idx) => cpy.ValDef(param)(rhs = selector(idx)) + } + Function(param :: Nil, Block(vdefs, body)) + } + /** Add annotation with class `cls` to tree: * tree @cls */ diff --git a/src/dotty/tools/dotc/typer/Typer.scala b/src/dotty/tools/dotc/typer/Typer.scala index 6a2ff30fa7c2..784702dbaa4a 100644 --- a/src/dotty/tools/dotc/typer/Typer.scala +++ b/src/dotty/tools/dotc/typer/Typer.scala @@ -611,26 +611,44 @@ class Typer extends Namer with TypeAssigner with Applications with Implicits wit if (protoFormals.length == params.length) protoFormals(i) else errorType(i"wrong number of parameters, expected: ${protoFormals.length}", tree.pos) - val inferredParams: List[untpd.ValDef] = - for ((param, i) <- params.zipWithIndex) yield - if (!param.tpt.isEmpty) param - else cpy.ValDef(param)( - tpt = untpd.TypeTree( - inferredParamType(param, protoFormal(i)).underlyingIfRepeated(isJava = false))) - - // Define result type of closure as the expected type, thereby pushing - // down any implicit searches. We do this even if the expected type is not fully - // defined, which is a bit of a hack. But it's needed to make the following work - // (see typers.scala and printers/PlainPrinter.scala for examples). - // - // def double(x: Char): String = s"$x$x" - // "abc" flatMap double - // - val resultTpt = protoResult match { - case WildcardType(_) => untpd.TypeTree() - case _ => untpd.TypeTree(protoResult) + /** Is `formal` a product type which is elementwise compatible with `params`? */ + def ptIsCorrectProduct(formal: Type) = { + val pclass = defn.ProductNClass(params.length) + isFullyDefined(formal, ForceDegree.noBottom) && + formal.derivesFrom(pclass) && + formal.baseArgTypes(pclass).corresponds(params) { + (argType, param) => + param.tpt.isEmpty || isCompatible(argType, typedAheadType(param.tpt).tpe) + } } - typed(desugar.makeClosure(inferredParams, fnBody, resultTpt), pt) + + val desugared = + if (protoFormals.length == 1 && params.length != 1 && ptIsCorrectProduct(protoFormals.head)) { + desugar.makeUnaryCaseLambda(params, fnBody) + } + else { + val inferredParams: List[untpd.ValDef] = + for ((param, i) <- params.zipWithIndex) yield + if (!param.tpt.isEmpty) param + else cpy.ValDef(param)( + tpt = untpd.TypeTree( + inferredParamType(param, protoFormal(i)).underlyingIfRepeated(isJava = false))) + + // Define result type of closure as the expected type, thereby pushing + // down any implicit searches. We do this even if the expected type is not fully + // defined, which is a bit of a hack. But it's needed to make the following work + // (see typers.scala and printers/PlainPrinter.scala for examples). + // + // def double(x: Char): String = s"$x$x" + // "abc" flatMap double + // + val resultTpt = protoResult match { + case WildcardType(_) => untpd.TypeTree() + case _ => untpd.TypeTree(protoResult) + } + desugar.makeClosure(inferredParams, fnBody, resultTpt) + } + typed(desugared, pt) } } diff --git a/test/dotc/tests.scala b/test/dotc/tests.scala index 12b83073832c..0f6d134a5327 100644 --- a/test/dotc/tests.scala +++ b/test/dotc/tests.scala @@ -111,6 +111,7 @@ class tests extends CompilerTest { @Test def neg_abstractOverride() = compileFile(negDir, "abstract-override", xerrors = 2) @Test def neg_blockescapes() = compileFile(negDir, "blockescapesNeg", xerrors = 1) @Test def neg_bounds() = compileFile(negDir, "bounds", xerrors = 2) + @Test def neg_functionArity() = compileFile(negDir, "function-arity", xerrors = 5) @Test def neg_typedapply() = compileFile(negDir, "typedapply", xerrors = 3) @Test def neg_typedIdents() = compileDir(negDir, "typedIdents", xerrors = 2) @Test def neg_assignments() = compileFile(negDir, "assignments", xerrors = 3) diff --git a/tests/neg/function-arity.scala b/tests/neg/function-arity.scala new file mode 100644 index 000000000000..83aa9448276e --- /dev/null +++ b/tests/neg/function-arity.scala @@ -0,0 +1,22 @@ +object Test { + + // From #873: + + trait X extends Function1[Int, String] + implicit def f2x(f: Function1[Int, String]): X = ??? + ({case _ if "".isEmpty => 0} : X) // error: expected String, found Int + + // Tests where parameter list cannot be made into a pattern + + def unary[T](x: T => Unit) = ??? + unary((x, y) => ()) // error + + unary[(Int, Int)]((x, y) => ()) + + unary[(Int, Int)](() => ()) // error + unary[(Int, Int)]((x, y, _) => ()) // error + + unary[(Int, Int)]((x: String, y) => ()) // error + + +} diff --git a/tests/pos/i873.scala b/tests/pos/function-arity.scala similarity index 61% rename from tests/pos/i873.scala rename to tests/pos/function-arity.scala index 94f8d2c67599..bb1a1cb8eb9f 100644 --- a/tests/pos/i873.scala +++ b/tests/pos/function-arity.scala @@ -7,4 +7,12 @@ object Test { ({case _ if "".isEmpty => ""} : X) // allowed, implicit view used to adapt // ({case _ if "".isEmpty => 0} : X) // expected String, found Int + + def unary[T](a: T, b: T, f: ((T, T)) => T): T = f((a, b)) + unary(1, 2, (x, y) => x) + unary(1, 2, (x: Int, y) => x) + unary(1, 2, (x: Int, y: Float) => x) + + val xs = List(1, 2, 3) + xs.zipWithIndex.map(_ + _) } From 62a526eeb664da5f0a7982375f0f77618b6b3f92 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 1 Nov 2015 10:23:43 +0100 Subject: [PATCH 2/7] Add more pos and neg tests Tests suggested by @retronym's comments on issue #897. --- tests/neg/function-arity.scala | 4 +++- tests/pos/function-arity.scala | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/neg/function-arity.scala b/tests/neg/function-arity.scala index 83aa9448276e..86fbab49ff12 100644 --- a/tests/neg/function-arity.scala +++ b/tests/neg/function-arity.scala @@ -18,5 +18,7 @@ object Test { unary[(Int, Int)]((x: String, y) => ()) // error - + def foo(a: Tuple2[Int, Int] => String): String = "" + def foo(a: Any => String) = () + foo((a: Int, b: String) => a + b) // error: none of the overloaded alternatives of method foo match arguments (Int, Int) } diff --git a/tests/pos/function-arity.scala b/tests/pos/function-arity.scala index bb1a1cb8eb9f..ec531c0ef544 100644 --- a/tests/pos/function-arity.scala +++ b/tests/pos/function-arity.scala @@ -14,5 +14,7 @@ object Test { unary(1, 2, (x: Int, y: Float) => x) val xs = List(1, 2, 3) + def f(x: Int, y: Int) = x * y xs.zipWithIndex.map(_ + _) + xs.zipWithIndex.map(f) } From 81b5f84de0d5a0d8ab2e0c5c5172145ee22d45dd Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Tue, 17 Nov 2015 23:33:19 +0100 Subject: [PATCH 3/7] Fix merge error --- src/dotty/tools/dotc/typer/Typer.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dotty/tools/dotc/typer/Typer.scala b/src/dotty/tools/dotc/typer/Typer.scala index 784702dbaa4a..952a1073c07a 100644 --- a/src/dotty/tools/dotc/typer/Typer.scala +++ b/src/dotty/tools/dotc/typer/Typer.scala @@ -613,7 +613,7 @@ class Typer extends Namer with TypeAssigner with Applications with Implicits wit /** Is `formal` a product type which is elementwise compatible with `params`? */ def ptIsCorrectProduct(formal: Type) = { - val pclass = defn.ProductNClass(params.length) + val pclass = defn.ProductNType(params.length).symbol isFullyDefined(formal, ForceDegree.noBottom) && formal.derivesFrom(pclass) && formal.baseArgTypes(pclass).corresponds(params) { From e5f8697cf54be6cfa82884eb2f4c4e4d79af2700 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Tue, 16 Feb 2016 15:44:54 +0100 Subject: [PATCH 4/7] Turn println into log --- src/dotty/tools/dotc/transform/DropEmptyCompanions.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dotty/tools/dotc/transform/DropEmptyCompanions.scala b/src/dotty/tools/dotc/transform/DropEmptyCompanions.scala index 550e3348f674..65362f19914a 100644 --- a/src/dotty/tools/dotc/transform/DropEmptyCompanions.scala +++ b/src/dotty/tools/dotc/transform/DropEmptyCompanions.scala @@ -40,7 +40,7 @@ class DropEmptyCompanions extends MiniPhaseTransform { thisTransform => case TypeDef(_, impl: Template) if tree.symbol.is(SyntheticModule) && tree.symbol.companionClass.exists && impl.body.forall(_.symbol.isPrimaryConstructor) => - println(i"removing ${tree.symbol}") + ctx.log(i"removing ${tree.symbol}") true case _ => false From 06bfbd379fe350a93e3de38940fda0e359a07e1d Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Tue, 16 Feb 2016 15:49:00 +0100 Subject: [PATCH 5/7] Strengthen requirement for auto-tupling Was: corresponding parameter types "are compatible". Now: corresponding parameter types "conform". This avoids the inconsistency mentioned by @retronym in #897. --- src/dotty/tools/dotc/typer/Typer.scala | 2 +- test/dotc/tests.scala | 2 +- tests/neg/function-arity.scala | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/dotty/tools/dotc/typer/Typer.scala b/src/dotty/tools/dotc/typer/Typer.scala index 952a1073c07a..f1e1d9286b11 100644 --- a/src/dotty/tools/dotc/typer/Typer.scala +++ b/src/dotty/tools/dotc/typer/Typer.scala @@ -618,7 +618,7 @@ class Typer extends Namer with TypeAssigner with Applications with Implicits wit formal.derivesFrom(pclass) && formal.baseArgTypes(pclass).corresponds(params) { (argType, param) => - param.tpt.isEmpty || isCompatible(argType, typedAheadType(param.tpt).tpe) + param.tpt.isEmpty || argType <:< typedAheadType(param.tpt).tpe } } diff --git a/test/dotc/tests.scala b/test/dotc/tests.scala index 0f6d134a5327..60e9a40880c0 100644 --- a/test/dotc/tests.scala +++ b/test/dotc/tests.scala @@ -111,7 +111,7 @@ class tests extends CompilerTest { @Test def neg_abstractOverride() = compileFile(negDir, "abstract-override", xerrors = 2) @Test def neg_blockescapes() = compileFile(negDir, "blockescapesNeg", xerrors = 1) @Test def neg_bounds() = compileFile(negDir, "bounds", xerrors = 2) - @Test def neg_functionArity() = compileFile(negDir, "function-arity", xerrors = 5) + @Test def neg_functionArity() = compileFile(negDir, "function-arity", xerrors = 7) @Test def neg_typedapply() = compileFile(negDir, "typedapply", xerrors = 3) @Test def neg_typedIdents() = compileDir(negDir, "typedIdents", xerrors = 2) @Test def neg_assignments() = compileFile(negDir, "assignments", xerrors = 3) diff --git a/tests/neg/function-arity.scala b/tests/neg/function-arity.scala index 86fbab49ff12..5e0cb1058f98 100644 --- a/tests/neg/function-arity.scala +++ b/tests/neg/function-arity.scala @@ -22,3 +22,7 @@ object Test { def foo(a: Any => String) = () foo((a: Int, b: String) => a + b) // error: none of the overloaded alternatives of method foo match arguments (Int, Int) } +object jasonComment { + implicit def i2s(i: Int): String = i.toString + ((x: String, y: String) => 42) : (((Int, Int)) => String) // error +} From 17296763448bf86c9f95b1458e5722b9829e8b3e Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Tue, 16 Feb 2016 16:11:09 +0100 Subject: [PATCH 6/7] Untuple using `def` not `val`. As retronym noted on #897, `val` forces to early. --- src/dotty/tools/dotc/ast/Desugar.scala | 9 +++++---- src/dotty/tools/dotc/typer/Typer.scala | 2 +- tests/run/function-arity.scala | 8 ++++++++ 3 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 tests/run/function-arity.scala diff --git a/src/dotty/tools/dotc/ast/Desugar.scala b/src/dotty/tools/dotc/ast/Desugar.scala index c1083d26d283..991940f10944 100644 --- a/src/dotty/tools/dotc/ast/Desugar.scala +++ b/src/dotty/tools/dotc/ast/Desugar.scala @@ -591,18 +591,19 @@ object desugar { /** Map n-ary function `(p1, ..., pn) => body` where n != 1 to unary function as follows: * * x$1 => { - * val p1 = x$1._1 + * def p1 = x$1._1 * ... - * val pn = x$1._n + * def pn = x$1._n * body * } */ - def makeUnaryCaseLambda(params: List[ValDef], body: Tree)(implicit ctx: Context): Tree = { + def makeTupledFunction(params: List[ValDef], body: Tree)(implicit ctx: Context): Tree = { val param = makeSyntheticParameter() def selector(n: Int) = Select(refOfDef(param), nme.selectorName(n)) val vdefs = params.zipWithIndex.map{ - case(param, idx) => cpy.ValDef(param)(rhs = selector(idx)) + case (param, idx) => + DefDef(param.name, Nil, Nil, TypeTree(), selector(idx)).withPos(param.pos) } Function(param :: Nil, Block(vdefs, body)) } diff --git a/src/dotty/tools/dotc/typer/Typer.scala b/src/dotty/tools/dotc/typer/Typer.scala index f1e1d9286b11..4d2ab3552d83 100644 --- a/src/dotty/tools/dotc/typer/Typer.scala +++ b/src/dotty/tools/dotc/typer/Typer.scala @@ -624,7 +624,7 @@ class Typer extends Namer with TypeAssigner with Applications with Implicits wit val desugared = if (protoFormals.length == 1 && params.length != 1 && ptIsCorrectProduct(protoFormals.head)) { - desugar.makeUnaryCaseLambda(params, fnBody) + desugar.makeTupledFunction(params, fnBody) } else { val inferredParams: List[untpd.ValDef] = diff --git a/tests/run/function-arity.scala b/tests/run/function-arity.scala new file mode 100644 index 000000000000..6d7e5bce10fb --- /dev/null +++ b/tests/run/function-arity.scala @@ -0,0 +1,8 @@ +object Test { + class T[A] { def foo(f: (=> A) => Int) = f(???) } + + def main(args: Array[String]): Unit = { + new T[(Int, Int)].foo((ii) => 0) + new T[(Int, Int)].foo((x, y) => 0) // check that this does not run into ??? + } +} From 4ceb3e7708f49b5d97af770dba226ca5a9c93c38 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Tue, 16 Feb 2016 17:17:40 +0100 Subject: [PATCH 7/7] Fix pos test Former test no longer allowed after conforms/compatible change. --- tests/pos/function-arity.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pos/function-arity.scala b/tests/pos/function-arity.scala index ec531c0ef544..9ba78105b68d 100644 --- a/tests/pos/function-arity.scala +++ b/tests/pos/function-arity.scala @@ -11,7 +11,7 @@ object Test { def unary[T](a: T, b: T, f: ((T, T)) => T): T = f((a, b)) unary(1, 2, (x, y) => x) unary(1, 2, (x: Int, y) => x) - unary(1, 2, (x: Int, y: Float) => x) + unary(1, 2, (x: Int, y: Int) => x) val xs = List(1, 2, 3) def f(x: Int, y: Int) = x * y