Skip to content

Commit

Permalink
Calm param autotupling for overloads (#21552)
Browse files Browse the repository at this point in the history
When resolving method overloads, we look to apply the same parameter
auto-tupling logic that we have in typedFunctionValue.  But we only
checked the function was unary without checking whether it was a tuple.
So I reused the same precondition.

Fixes #16108
  • Loading branch information
odersky authored Sep 23, 2024
2 parents 3097a84 + 9f90ad0 commit e5f7272
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 18 deletions.
30 changes: 22 additions & 8 deletions compiler/src/dotty/tools/dotc/typer/Applications.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2214,19 +2214,26 @@ trait Applications extends Compatibility {
case untpd.Function(args: List[untpd.ValDef] @unchecked, body) =>

// If ref refers to a method whose parameter at index `idx` is a function type,
// the arity of that function, otherise -1.
def paramCount(ref: TermRef) =
// the parameters of that function, otherwise Nil.
// We return Nil for both nilary functions and non-functions,
// because we won't be making tupled functions for nilary functions anyways,
// seeing as there is no Tuple0.
def params(ref: TermRef) =
val formals = ref.widen.firstParamTypes
if formals.length > idx then
formals(idx).dealias match
case defn.FunctionNOf(args, _, _) => args.length
case _ => -1
else -1
case defn.FunctionNOf(args, _, _) => args
case _ => Nil
else Nil

def isCorrectUnaryFunction(alt: TermRef): Boolean =
val formals = params(alt)
formals.length == 1 && ptIsCorrectProduct(formals.head, args)

val numArgs = args.length
if numArgs != 1
&& !alts.exists(paramCount(_) == numArgs)
&& alts.exists(paramCount(_) == 1)
if numArgs > 1
&& !alts.exists(params(_).lengthIs == numArgs)
&& alts.exists(isCorrectUnaryFunction)
then
desugar.makeTupledFunction(args, body, isGenericTuple = true)
// `isGenericTuple = true` is the safe choice here. It means the i'th tuple
Expand Down Expand Up @@ -2395,6 +2402,13 @@ trait Applications extends Compatibility {
}
end resolveOverloaded1

/** Is `formal` a product type which is elementwise compatible with `params`? */
def ptIsCorrectProduct(formal: Type, params: List[untpd.ValDef])(using Context): Boolean =
isFullyDefined(formal, ForceDegree.flipBottom)
&& defn.isProductSubType(formal)
&& tupleComponentTypes(formal).corresponds(params): (argType, param) =>
param.tpt.isEmpty || argType.widenExpr <:< typedAheadType(param.tpt).tpe

/** The largest suffix of `paramss` that has the same first parameter name as `t`,
* plus the number of term parameters in `paramss` that come before that suffix.
*/
Expand Down
11 changes: 1 addition & 10 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1837,19 +1837,10 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
if (protoFormals.length == params.length) (protoFormals(i), isDefinedErased(i))
else (errorType(WrongNumberOfParameters(tree, params.length, pt, protoFormals.length), tree.srcPos), false)

/** Is `formal` a product type which is elementwise compatible with `params`? */
def ptIsCorrectProduct(formal: Type) =
isFullyDefined(formal, ForceDegree.flipBottom) &&
defn.isProductSubType(formal) &&
tupleComponentTypes(formal).corresponds(params) {
(argType, param) =>
param.tpt.isEmpty || argType.widenExpr <:< typedAheadType(param.tpt).tpe
}

var desugared: untpd.Tree = EmptyTree
if protoFormals.length == 1 && params.length != 1 then
val firstFormal = protoFormals.head.loBound
if ptIsCorrectProduct(firstFormal) then
if ptIsCorrectProduct(firstFormal, params) then
val isGenericTuple =
firstFormal.derivesFrom(defn.TupleClass)
&& !defn.isTupleClass(firstFormal.typeSymbol)
Expand Down
41 changes: 41 additions & 0 deletions tests/run/i16108.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import scala.language.implicitConversions

final class Functoid[+R](val function: Product => R)

object Functoid {
implicit def apply[A, R](function: A => R): Functoid[R] = {
println(s"arity 1")
new Functoid({ case Tuple1(a: A @unchecked) => function(a) })
}
implicit def apply[A, B, R](function: (A, B) => R): Functoid[R] = {
println("arity 2")
new Functoid({ case (a: A @unchecked, b: B @unchecked) => function(a, b) })
}
}

final case class ContainerConfig(image: String, version: Int, cmd: String)

final class ContainerResource

object ContainerResource {
implicit final class DockerProviderExtensions(private val self: Functoid[ContainerResource]) extends AnyVal {
def modifyConfig(modify: Functoid[ContainerConfig => ContainerConfig]): Functoid[ContainerConfig => ContainerConfig] = modify
// removing this overload fixes the implicit conversion and returns `arity 2` print
def modifyConfig(modify: ContainerConfig => ContainerConfig): Functoid[ContainerConfig => ContainerConfig] = new Functoid(_ => modify)
}
}

object Test {
def main(args: Array[String]): Unit = {
val cfg = new Functoid(_ => new ContainerResource)
.modifyConfig {
// applying Functoid.apply explicitly instead of via implicit conversion also avoids untupling
// Functoid {
(image: String, version: Int) => (cfg: ContainerConfig) => cfg.copy(image, version)
// }
}
.function.apply(Tuple2("img", 9))
.apply(ContainerConfig("a", 0, "b"))
println(cfg)
}
}

0 comments on commit e5f7272

Please sign in to comment.