From 3b617dff864db0e781092d2144a97ff20668e879 Mon Sep 17 00:00:00 2001 From: Solal Pirelli Date: Tue, 28 Jul 2026 15:35:27 +0200 Subject: [PATCH 1/2] Clearer error message when SAM conversion is rejected for a sealed trait --- .../src/dotty/tools/dotc/core/Types.scala | 24 +++++++++++-------- .../dotty/tools/dotc/reporting/messages.scala | 6 ++++- tests/neg/closure-to-sealed-trait.check | 8 +++++++ tests/neg/closure-to-sealed-trait.scala | 4 ++++ 4 files changed, 31 insertions(+), 11 deletions(-) create mode 100644 tests/neg/closure-to-sealed-trait.check create mode 100644 tests/neg/closure-to-sealed-trait.scala diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 7e0867f087eb..5c7acdb766db 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -6123,7 +6123,7 @@ object Types extends TypeUtils { approxWildcardArgs(tp) end samParent - def samClass(tp: Type)(using Context): Symbol = tp match + private def samClass(tp: Type, ignoreFinalOrSealed: Boolean)(using Context): Symbol = tp match case tp: ClassInfo => val cls = tp.cls def takesNoArgs(tp: Type) = @@ -6135,28 +6135,32 @@ object Types extends TypeUtils { takesNoArgs(tp) && (!cls.is(Trait) || takesNoArgs(tp.parents.head)) def isInstantiable = - !cls.isOneOf(FinalOrSealed) && (tp.appliedRef <:< tp.selfType) + (ignoreFinalOrSealed || !cls.isOneOf(FinalOrSealed)) && (tp.appliedRef <:< tp.selfType) if noArgsNeeded && isInstantiable then cls else NoSymbol case tp: AppliedType => - samClass(tp.superType) + samClass(tp.superType, ignoreFinalOrSealed) case tp: TypeRef => - samClass(tp.underlying) + samClass(tp.underlying, ignoreFinalOrSealed) case tp: RefinedType => - samClass(tp.underlying) + samClass(tp.underlying, ignoreFinalOrSealed) case tp: TypeBounds => - samClass(tp.underlying) + samClass(tp.underlying, ignoreFinalOrSealed) case tp: TypeVar => - samClass(tp.underlying) + samClass(tp.underlying, ignoreFinalOrSealed) case tp: AnnotatedType => - samClass(tp.underlying) + samClass(tp.underlying, ignoreFinalOrSealed) case tp: FlexibleType => - samClass(tp.underlying) + samClass(tp.underlying, ignoreFinalOrSealed) case _ => NoSymbol def unapply(tp: Type)(using Context): Option[(MethodType, Type)] = - val cls = samClass(tp) + unapply(tp, false) + + /** Optionally ignores `final` and `sealed`, so that error messages can point to this specific problem. */ + def unapply(tp: Type, ignoreFinalOrSealed: Boolean)(using Context): Option[(MethodType, Type)] = + val cls = samClass(tp, ignoreFinalOrSealed) if cls.exists then val absMems = if tp.isRef(defn.PartialFunctionClass) then diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index 7e5fbb1d17ed..248ea6e90a1c 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -359,8 +359,12 @@ class TypeMismatch(val found: Type, expected: Type, val inTree: Option[untpd.Tre if (found1 frozen_<:< expected1) || reported.fbounded then (found, expected) else (found1, expected1) val (foundStr, expectedStr) = Formatting.typeDiff(found2.normalized, expected2.normalized) + val extra = + if defn.isFunctionNType(found) && SAMType.unapply(expected).isEmpty && SAMType.unapply(expected, ignoreFinalOrSealed = true).nonEmpty + then i"\nNote that conversion from a function to a single abstract method type is only possible if the type is neither ${hl("final")} nor ${hl("sealed")}" + else "" i"""|${preface}Found: $foundStr - |Required: $expectedStr${reported.notes}""" + |Required: $expectedStr${reported.notes}$extra""" end msg override def msgPostscript(using Context): String = diff --git a/tests/neg/closure-to-sealed-trait.check b/tests/neg/closure-to-sealed-trait.check new file mode 100644 index 000000000000..f96a2b63ce02 --- /dev/null +++ b/tests/neg/closure-to-sealed-trait.check @@ -0,0 +1,8 @@ +-- [E007] Type Mismatch Error: tests/neg/closure-to-sealed-trait.scala:4:18 -------------------------------------------- +4 |def test: Foo = { (x: String) => "" } // error + | ^^^^^^^^^^^^^^^^^ + |Found: String => String + |Required: Foo + |Note that conversion from a function to a single abstract method type is only possible if the type is neither final nor sealed + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/closure-to-sealed-trait.scala b/tests/neg/closure-to-sealed-trait.scala new file mode 100644 index 000000000000..8ca3ac494d38 --- /dev/null +++ b/tests/neg/closure-to-sealed-trait.scala @@ -0,0 +1,4 @@ +sealed trait Foo { + def foo(x: String): String +} +def test: Foo = { (x: String) => "" } // error From 244855200185435b4bed3eeb5914ab3083225e71 Mon Sep 17 00:00:00 2001 From: Solal Pirelli Date: Wed, 12 Aug 2026 14:32:04 +0200 Subject: [PATCH 2/2] PR feedback --- .../src/dotty/tools/dotc/core/Types.scala | 74 ++++++++++--------- .../dotty/tools/dotc/reporting/messages.scala | 4 +- 2 files changed, 42 insertions(+), 36 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 5c7acdb766db..30c20ded03ff 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -6123,44 +6123,48 @@ object Types extends TypeUtils { approxWildcardArgs(tp) end samParent - private def samClass(tp: Type, ignoreFinalOrSealed: Boolean)(using Context): Symbol = tp match - case tp: ClassInfo => - val cls = tp.cls - def takesNoArgs(tp: Type) = - !tp.classSymbol.primaryConstructor.exists - // e.g. `ContextFunctionN` does not have constructors - || tp.applicableConstructors(argTypes = Nil, adaptVarargs = true).lengthCompare(1) == 0 - // we require a unique constructor so that SAM expansion is deterministic - val noArgsNeeded: Boolean = - takesNoArgs(tp) - && (!cls.is(Trait) || takesNoArgs(tp.parents.head)) - def isInstantiable = - (ignoreFinalOrSealed || !cls.isOneOf(FinalOrSealed)) && (tp.appliedRef <:< tp.selfType) - if noArgsNeeded && isInstantiable then cls - else NoSymbol - case tp: AppliedType => - samClass(tp.superType, ignoreFinalOrSealed) - case tp: TypeRef => - samClass(tp.underlying, ignoreFinalOrSealed) - case tp: RefinedType => - samClass(tp.underlying, ignoreFinalOrSealed) - case tp: TypeBounds => - samClass(tp.underlying, ignoreFinalOrSealed) - case tp: TypeVar => - samClass(tp.underlying, ignoreFinalOrSealed) - case tp: AnnotatedType => - samClass(tp.underlying, ignoreFinalOrSealed) - case tp: FlexibleType => - samClass(tp.underlying, ignoreFinalOrSealed) - case _ => - NoSymbol - def unapply(tp: Type)(using Context): Option[(MethodType, Type)] = - unapply(tp, false) + deconstruct(tp, false) /** Optionally ignores `final` and `sealed`, so that error messages can point to this specific problem. */ - def unapply(tp: Type, ignoreFinalOrSealed: Boolean)(using Context): Option[(MethodType, Type)] = - val cls = samClass(tp, ignoreFinalOrSealed) + def deconstruct(tp: Type, ignoreFinalOrSealed: Boolean)(using Context): Option[(MethodType, Type)] = + def samClass(tp: Type)(using Context): Symbol = tp match { + case tp: ClassInfo => + val cls = tp.cls + + def takesNoArgs(tp: Type) = + !tp.classSymbol.primaryConstructor.exists + // e.g. `ContextFunctionN` does not have constructors + || tp.applicableConstructors(argTypes = Nil, adaptVarargs = true).lengthCompare(1) == 0 + + // we require a unique constructor so that SAM expansion is deterministic + val noArgsNeeded: Boolean = + takesNoArgs(tp) + && (!cls.is(Trait) || takesNoArgs(tp.parents.head)) + + def isInstantiable = + (ignoreFinalOrSealed || !cls.isOneOf(FinalOrSealed)) && (tp.appliedRef <:< tp.selfType) + + if noArgsNeeded && isInstantiable then cls + else NoSymbol + case tp: AppliedType => + samClass(tp.superType) + case tp: TypeRef => + samClass(tp.underlying) + case tp: RefinedType => + samClass(tp.underlying) + case tp: TypeBounds => + samClass(tp.underlying) + case tp: TypeVar => + samClass(tp.underlying) + case tp: AnnotatedType => + samClass(tp.underlying) + case tp: FlexibleType => + samClass(tp.underlying) + case _ => + NoSymbol + } + val cls = samClass(tp) if cls.exists then val absMems = if tp.isRef(defn.PartialFunctionClass) then diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index 248ea6e90a1c..ba4f76fa7d04 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -360,7 +360,9 @@ class TypeMismatch(val found: Type, expected: Type, val inTree: Option[untpd.Tre else (found1, expected1) val (foundStr, expectedStr) = Formatting.typeDiff(found2.normalized, expected2.normalized) val extra = - if defn.isFunctionNType(found) && SAMType.unapply(expected).isEmpty && SAMType.unapply(expected, ignoreFinalOrSealed = true).nonEmpty + if defn.isFunctionNType(found) + && SAMType.deconstruct(expected, ignoreFinalOrSealed = false).isEmpty + && SAMType.deconstruct(expected, ignoreFinalOrSealed = true).nonEmpty then i"\nNote that conversion from a function to a single abstract method type is only possible if the type is neither ${hl("final")} nor ${hl("sealed")}" else "" i"""|${preface}Found: $foundStr