From 73b0e75297dc56954722590f7fba0b9fb0fee85b Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Mon, 22 May 2023 15:27:38 +0200 Subject: [PATCH 1/2] Type parameter clause inference for lambdas When a lambda is written without a type parameter clause but the expected type is a polymorphic function type, try to adapt the lambda into a polymorphic lambda by inferring an appropriate type parameter clause. This change broke one example in spire which relied on implicit conversions. The fix has been accepted upstream: https://github.com/typelevel/spire/pull/1247 --- community-build/community-projects/spire | 2 +- .../src/dotty/tools/dotc/config/Feature.scala | 1 + .../src/dotty/tools/dotc/typer/Typer.scala | 26 +++++++++++++++++++ .../runtime/stdLibPatches/language.scala | 3 +++ tests/neg/typeClauseInference.scala | 3 +++ tests/pos/typeClauseInference.scala | 11 ++++++++ 6 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 tests/neg/typeClauseInference.scala create mode 100644 tests/pos/typeClauseInference.scala diff --git a/community-build/community-projects/spire b/community-build/community-projects/spire index bc524eeea735..a74ee078261e 160000 --- a/community-build/community-projects/spire +++ b/community-build/community-projects/spire @@ -1 +1 @@ -Subproject commit bc524eeea735a3cf4d5108039f95950b024a14e4 +Subproject commit a74ee078261edd320702dbb9677423d7200738ea diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index 5bcc139326f9..817172efd0a1 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -33,6 +33,7 @@ object Feature: val pureFunctions = experimental("pureFunctions") val captureChecking = experimental("captureChecking") val into = experimental("into") + val typeClauseInference = experimental("typeClauseInference") val globalOnlyImports: Set[TermName] = Set(pureFunctions, captureChecking) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 3aaf4fec59d6..22aae4b75978 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1462,6 +1462,32 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer def typedFunctionValue(tree: untpd.Function, pt: Type)(using Context): Tree = { val untpd.Function(params: List[untpd.ValDef] @unchecked, _) = tree: @unchecked + if Feature.enabled(Feature.typeClauseInference) then + // If the expected type is a polymorphic function type: + // + // [S_1, ..., S_m] => (T_1, ..., T_n) => R + // (where each S_i might have type bounds) + // + // and we are typing a lambda of the form: + // + // (x_1, ..., x_n) => e + // (where each x_i might have a type ascription) + // + // then continue with an inferred type parameter clause: + // + // [S'_1, ..., S'_m] => (x_1, ..., x_n) => e + // (where each S'_i is fresh and has the bounds of S_i after substituting S_j by S'_j for all j) + pt match + case defn.PolyFunctionOf(poly @ PolyType(_, mt: MethodType)) + if params.lengthCompare(mt.paramNames) == 0 => + val tparams = poly.paramNames.lazyZip(poly.paramInfos).map: (name, info) => + untpd.TypeDef(UniqueName.fresh(name), untpd.InLambdaTypeTree(isResult = false, (tsyms, vsyms) => + info.substParams(poly, tsyms.map(_.typeRef)) + )).withFlags(SyntheticParam) + .withSpan(tree.span.startPos) + return typed(untpd.PolyFunction(tparams, tree), pt) + case _ => + val (isContextual, isDefinedErased) = tree match { case tree: untpd.FunctionWithMods => (tree.mods.is(Given), tree.erasedParams) case _ => (false, tree.args.map(_ => false)) diff --git a/library/src/scala/runtime/stdLibPatches/language.scala b/library/src/scala/runtime/stdLibPatches/language.scala index 091e75fa06e1..37bbbd0233de 100644 --- a/library/src/scala/runtime/stdLibPatches/language.scala +++ b/library/src/scala/runtime/stdLibPatches/language.scala @@ -98,6 +98,9 @@ object language: */ @compileTimeOnly("`into` can only be used at compile time in import statements") object into + + @compileTimeOnly("`typeClauseInference` can only be used at compile time in import statements") + object typeClauseInference end experimental /** The deprecated object contains features that are no longer officially suypported in Scala. diff --git a/tests/neg/typeClauseInference.scala b/tests/neg/typeClauseInference.scala new file mode 100644 index 000000000000..b175b5f53b3b --- /dev/null +++ b/tests/neg/typeClauseInference.scala @@ -0,0 +1,3 @@ +import scala.language.experimental.typeClauseInference + +val notInScopeInferred: [T] => T => T = x => (x: T) // error diff --git a/tests/pos/typeClauseInference.scala b/tests/pos/typeClauseInference.scala new file mode 100644 index 000000000000..47d673a5e622 --- /dev/null +++ b/tests/pos/typeClauseInference.scala @@ -0,0 +1,11 @@ +import language.experimental.typeClauseInference + +class Test: + def test: Unit = + val it1: [T] => T => T = x => x + val it2: [T] => (T, Int) => T = (x, y: Int) => x + val it3: [T, S <: List[T]] => (T, S) => List[T] = (x, y) => x :: y + val tuple1: (String, String) = (1, 2.0).map[[_] =>> String](_.toString) + val tuple2: (List[Int], List[Double]) = (1, 2.0).map(List(_)) + // Not supported yet, require eta-expansion with a polymorphic expected type + // val tuple3: (List[Int], List[Double]) = (1, 2.0).map(List.apply) From 183d84e378b9625b083c2eb67d5fc5084553909e Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Fri, 7 Jul 2023 19:14:13 +0200 Subject: [PATCH 2/2] Eta-expand methods even if the expected type is a polymorphic function In that case, let eta-expansion produce an untyped monomorphic lambda as usual. Thanks to the previous commit, a type parameter clause for this lambda will be inferred if possible. --- .../src/dotty/tools/dotc/typer/Typer.scala | 23 +++++++++++++++++++ tests/neg/typeClauseInference.scala | 4 ++++ tests/pos/typeClauseInference.scala | 13 +++++++++-- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 22aae4b75978..7e9c12164c57 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -4344,6 +4344,29 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer if isApplyProxy(tree) then newExpr else if pt.isInstanceOf[PolyProto] then tree else + if Feature.enabled(Feature.typeClauseInference) then + // If `tree` is a polymorphic method reference and the expected + // type is a polymorphic function, perform a monomorphic + // eta-expansion of the method reference. + // For example, this means that + // + // (1, 2.0).map(Option.apply) + // + // will expand to: + // + // (1, 2.0).map(x => Option.apply(x)) + // + // A type parameter clause for the lambda will subsequently be + // inferred (from its expected type) in typedFunctionValue. + pt match + case defn.PolyFunctionOf(_: PolyType) => + poly.resultType match + case mt: MethodType => + val expanded = etaExpand(tree, mt, mt.paramInfos.length) + return simplify(typed(expanded, pt), pt, locked) + case _ => + case _ => + end if var typeArgs = tree match case Select(qual, nme.CONSTRUCTOR) => qual.tpe.widenDealias.argTypesLo.map(TypeTree(_)) case _ => Nil diff --git a/tests/neg/typeClauseInference.scala b/tests/neg/typeClauseInference.scala index b175b5f53b3b..db448f6567a1 100644 --- a/tests/neg/typeClauseInference.scala +++ b/tests/neg/typeClauseInference.scala @@ -1,3 +1,7 @@ import scala.language.experimental.typeClauseInference val notInScopeInferred: [T] => T => T = x => (x: T) // error + +def bar[A]: A => A = x => x +val barf1: [T] => T => T = bar(_) // ok +val barf2: [T] => T => T = bar // error, unlike in the original SIP-49. diff --git a/tests/pos/typeClauseInference.scala b/tests/pos/typeClauseInference.scala index 47d673a5e622..485b7b083c22 100644 --- a/tests/pos/typeClauseInference.scala +++ b/tests/pos/typeClauseInference.scala @@ -7,5 +7,14 @@ class Test: val it3: [T, S <: List[T]] => (T, S) => List[T] = (x, y) => x :: y val tuple1: (String, String) = (1, 2.0).map[[_] =>> String](_.toString) val tuple2: (List[Int], List[Double]) = (1, 2.0).map(List(_)) - // Not supported yet, require eta-expansion with a polymorphic expected type - // val tuple3: (List[Int], List[Double]) = (1, 2.0).map(List.apply) + + // Eta-expansion + val e1: [T] => T => Option[T] = Option.apply + val tuple3: (Option[Int], Option[Double]) = (1, 2.0).map(Option.apply) + + // Eta-expansion that wouldn't work with the original SIP-49 + def pair[S, T](x: S, y: T): (S, T) = (x, y) + val f5: [T] => (Int, T) => (Int, T) = pair + val f6: [T] => (T, Int) => (T, Int) = pair + def id[T](x: T): T = x + val f7: [S] => List[S] => List[S] = id