From 2b6237eaee6b145b95ae3285e3dbd288b6d1a86f Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Wed, 22 Oct 2025 17:38:36 +0200 Subject: [PATCH 1/5] Try to instantiate `pt` if this is possible by looking into union/intersection types --- compiler/src/dotty/tools/dotc/typer/Typer.scala | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index fc71c0e43034..25eb570500e8 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1929,15 +1929,13 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer NoType } - pt.stripNull() match { - case pt: TypeVar - if untpd.isFunctionWithUnknownParamType(tree) && !calleeType.exists => - // try to instantiate `pt` if this is possible. If it does not - // work the error will be reported later in `inferredParam`, - // when we try to infer the parameter type. - isFullyDefined(pt, ForceDegree.flipBottom) - case _ => - } + if pt.existsPart(_.isInstanceOf[TypeVar], StopAt.Static) + && untpd.isFunctionWithUnknownParamType(tree) + && !calleeType.exists then + // try to instantiate `pt` if this is possible. If it does not + // work the error will be reported later in `inferredParam`, + // when we try to infer the parameter type. + isFullyDefined(pt, ForceDegree.flipBottom) val (protoFormals, resultTpt) = decomposeProtoFunction(pt, params.length, tree.srcPos) From a3d68038a5a5e9f99eeaca4022a94ed1ffcc68c1 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Thu, 23 Oct 2025 10:55:23 +0200 Subject: [PATCH 2/5] Refactor type instantiation logic when typing function values to handle union types --- compiler/src/dotty/tools/dotc/typer/Typer.scala | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 25eb570500e8..5b78a6123339 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1929,13 +1929,21 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer NoType } - if pt.existsPart(_.isInstanceOf[TypeVar], StopAt.Static) - && untpd.isFunctionWithUnknownParamType(tree) - && !calleeType.exists then + def instantiateInUnion(tp: Type): Unit = tp match + case tp: OrType => + instantiateInUnion(tp.tp1) + instantiateInUnion(tp.tp2) + case tp: FlexibleType => + instantiateInUnion(tp.hi) + case tp: TypeVar => + isFullyDefined(tp, ForceDegree.flipBottom) + case _ => + + if untpd.isFunctionWithUnknownParamType(tree) && !calleeType.exists then // try to instantiate `pt` if this is possible. If it does not // work the error will be reported later in `inferredParam`, // when we try to infer the parameter type. - isFullyDefined(pt, ForceDegree.flipBottom) + instantiateInUnion(pt) val (protoFormals, resultTpt) = decomposeProtoFunction(pt, params.length, tree.srcPos) From ea603c1ebb6634391a2a4142679afbd0201a6319 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Thu, 23 Oct 2025 11:29:53 +0200 Subject: [PATCH 3/5] Update comments; add test cases for function type inference with union types. --- .../src/dotty/tools/dotc/typer/Typer.scala | 16 +++++----- tests/pos/infer-function-type-in-union.scala | 32 +++++++++++++++++++ 2 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 tests/pos/infer-function-type-in-union.scala diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 5b78a6123339..e2be1a0cda3c 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1929,21 +1929,21 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer NoType } - def instantiateInUnion(tp: Type): Unit = tp match + def tryToInstantiateInUnion(tp: Type): Unit = tp match case tp: OrType => - instantiateInUnion(tp.tp1) - instantiateInUnion(tp.tp2) + tryToInstantiateInUnion(tp.tp1) + tryToInstantiateInUnion(tp.tp2) case tp: FlexibleType => - instantiateInUnion(tp.hi) + tryToInstantiateInUnion(tp.hi) case tp: TypeVar => isFullyDefined(tp, ForceDegree.flipBottom) case _ => if untpd.isFunctionWithUnknownParamType(tree) && !calleeType.exists then - // try to instantiate `pt` if this is possible. If it does not - // work the error will be reported later in `inferredParam`, - // when we try to infer the parameter type. - instantiateInUnion(pt) + // Try to instantiate `pt` when possible, including type variables in union types + // to help finding function types. If it does not work the error will be reported + // later in `inferredParam`, when we try to infer the parameter type. + tryToInstantiateInUnion(pt) val (protoFormals, resultTpt) = decomposeProtoFunction(pt, params.length, tree.srcPos) diff --git a/tests/pos/infer-function-type-in-union.scala b/tests/pos/infer-function-type-in-union.scala new file mode 100644 index 000000000000..dcd3edcd8987 --- /dev/null +++ b/tests/pos/infer-function-type-in-union.scala @@ -0,0 +1,32 @@ + +def f[T](x: T): T = ??? +def f2[T](x: T | T): T = ??? +def f3[T](x: T | Null): T = ??? +def f4[T](x: Int | T): T = ??? + +trait MyOption[+T] + +object MyOption: + def apply[T](x: T | Null): MyOption[T] = ??? + +def test = + val g: AnyRef => Boolean = f { + x => x eq null // ok + } + val g2: AnyRef => Boolean = f2 { + x => x eq null // ok + } + val g3: AnyRef => Boolean = f3 { + x => x eq null // was error + } + val g4: AnyRef => Boolean = f4 { + x => x eq null // was error + } + + val o1: MyOption[String] = MyOption(null) + val o2: MyOption[String => Boolean] = MyOption { + x => x.length > 0 + } + val o3: MyOption[(String, String) => Boolean] = MyOption { + (x, y) => x.length > y.length + } \ No newline at end of file From df5e0c9c179f32fb394508c2aee228ee5af39307 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Thu, 23 Oct 2025 14:20:45 +0200 Subject: [PATCH 4/5] Try to only instantiate one type variable bounded function types --- .../src/dotty/tools/dotc/typer/Typer.scala | 40 ++++++++++++++----- tests/neg-custom-args/captures/i15923.check | 2 +- tests/pos/infer-function-type-in-union.scala | 12 +++++- 3 files changed, 41 insertions(+), 13 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index e2be1a0cda3c..f6508266dc2c 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1929,21 +1929,39 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer NoType } - def tryToInstantiateInUnion(tp: Type): Unit = tp match - case tp: OrType => - tryToInstantiateInUnion(tp.tp1) - tryToInstantiateInUnion(tp.tp2) + /** Try to instantiate one type variable bounded by function types that appear + * deeply inside `tp`, including union or intersection types. + */ + def tryToInstantiateDeeply(tp: Type): Boolean = tp match + case tp: AndOrType => + tryToInstantiateDeeply(tp.tp1) + || tryToInstantiateDeeply(tp.tp2) case tp: FlexibleType => - tryToInstantiateInUnion(tp.hi) - case tp: TypeVar => + tryToInstantiateDeeply(tp.hi) + case tp: TypeVar if isConstrainedByFunctionType(tp) => + // Only instantiate if the type variable is constrained by function types isFullyDefined(tp, ForceDegree.flipBottom) - case _ => + case _ => false + + def isConstrainedByFunctionType(tvar: TypeVar): Boolean = + val origin = tvar.origin + val bounds = ctx.typerState.constraint.bounds(origin) + def containsFunctionType(tp: Type): Boolean = tp.dealias match + case tp if defn.isFunctionType(tp) => true + case SAMType(_, _) => true + case tp: AndOrType => + containsFunctionType(tp.tp1) || containsFunctionType(tp.tp2) + case tp: FlexibleType => + containsFunctionType(tp.hi) + case _ => false + containsFunctionType(bounds.lo) || containsFunctionType(bounds.hi) if untpd.isFunctionWithUnknownParamType(tree) && !calleeType.exists then - // Try to instantiate `pt` when possible, including type variables in union types - // to help finding function types. If it does not work the error will be reported - // later in `inferredParam`, when we try to infer the parameter type. - tryToInstantiateInUnion(pt) + // Try to instantiate `pt` when possible, by searching a nested type variable + // bounded by function types to help infer parameter types. + // If it does not work the error will be reported later in `inferredParam`, + // when we try to infer the parameter type. + tryToInstantiateDeeply(pt) val (protoFormals, resultTpt) = decomposeProtoFunction(pt, params.length, tree.srcPos) diff --git a/tests/neg-custom-args/captures/i15923.check b/tests/neg-custom-args/captures/i15923.check index e018a2ea9f4b..76341539fc9e 100644 --- a/tests/neg-custom-args/captures/i15923.check +++ b/tests/neg-custom-args/captures/i15923.check @@ -6,7 +6,7 @@ | |Note that capability lcap cannot be included in outer capture set 's1 of parameter cap. | - |where: => refers to a fresh root capability created in anonymous function of type (using lcap: scala.caps.Capability): test2.Cap^{lcap} -> [T] => (op: test2.Cap^{lcap} => T) -> T when instantiating expected result type test2.Cap^{lcap} ->{cap²} [T] => (op: test2.Cap^'s6 ->'s7 T) ->'s8 T of function literal + |where: => refers to a fresh root capability created in anonymous function of type (using lcap: scala.caps.Capability): test2.Cap^{lcap} -> [T] => (op: test2.Cap => T) -> T when instantiating expected result type test2.Cap^{lcap} ->{cap²} [T] => (op: test2.Cap^'s6 ->'s7 T) ->'s8 T of function literal | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15923.scala:12:21 --------------------------------------- diff --git a/tests/pos/infer-function-type-in-union.scala b/tests/pos/infer-function-type-in-union.scala index dcd3edcd8987..f631761b3897 100644 --- a/tests/pos/infer-function-type-in-union.scala +++ b/tests/pos/infer-function-type-in-union.scala @@ -29,4 +29,14 @@ def test = } val o3: MyOption[(String, String) => Boolean] = MyOption { (x, y) => x.length > y.length - } \ No newline at end of file + } + + +class Box[T] +val box: Box[Unit] = ??? +def ff1[T, U](x: T | U, y: Box[U]): T = ??? +def ff2[T, U](x: T & U): T = ??? + +def test2 = + val a1: Any => Any = ff1(x => x, box) + val a2: Any => Any = ff2(x => x) \ No newline at end of file From 201c25a1a6b696fa25faa1d1621178e8d15b28aa Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Wed, 29 Oct 2025 14:27:05 +0100 Subject: [PATCH 5/5] Match the single TypeVar case with the previous logic --- compiler/src/dotty/tools/dotc/typer/Typer.scala | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index f6508266dc2c..bb01d89ad017 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1932,7 +1932,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer /** Try to instantiate one type variable bounded by function types that appear * deeply inside `tp`, including union or intersection types. */ - def tryToInstantiateDeeply(tp: Type): Boolean = tp match + def tryToInstantiateDeeply(tp: Type): Boolean = tp.dealias match case tp: AndOrType => tryToInstantiateDeeply(tp.tp1) || tryToInstantiateDeeply(tp.tp2) @@ -1946,6 +1946,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer def isConstrainedByFunctionType(tvar: TypeVar): Boolean = val origin = tvar.origin val bounds = ctx.typerState.constraint.bounds(origin) + // The search is done by the best-effort, and we don't look into TypeVars recursively. def containsFunctionType(tp: Type): Boolean = tp.dealias match case tp if defn.isFunctionType(tp) => true case SAMType(_, _) => true @@ -1957,11 +1958,15 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer containsFunctionType(bounds.lo) || containsFunctionType(bounds.hi) if untpd.isFunctionWithUnknownParamType(tree) && !calleeType.exists then - // Try to instantiate `pt` when possible, by searching a nested type variable - // bounded by function types to help infer parameter types. + // Try to instantiate `pt` when possible. + // * If `pt` is a type variable, we try to instantiate it directly. + // * If `pt` is a more complex type, we try to instantiate it deeply by searching + // a nested type variable bounded by function types to help infer parameter types. // If it does not work the error will be reported later in `inferredParam`, // when we try to infer the parameter type. - tryToInstantiateDeeply(pt) + pt match + case pt: TypeVar => isFullyDefined(pt, ForceDegree.flipBottom) + case _ => tryToInstantiateDeeply(pt) val (protoFormals, resultTpt) = decomposeProtoFunction(pt, params.length, tree.srcPos)