diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 2fec0683c410..6d493a48364d 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -762,6 +762,7 @@ class Definitions { @tu lazy val LanguageDeprecatedModule: Symbol = requiredModule("scala.language.deprecated") @tu lazy val NonLocalReturnControlClass: ClassSymbol = requiredClass("scala.runtime.NonLocalReturnControl") @tu lazy val SelectableClass: ClassSymbol = requiredClass("scala.Selectable") + @tu lazy val WithoutPreciseParameterTypesClass: Symbol = requiredClass("scala.Selectable.WithoutPreciseParameterTypes") @tu lazy val ReflectPackageClass: Symbol = requiredPackage("scala.reflect.package").moduleClass @tu lazy val ClassTagClass: ClassSymbol = requiredClass("scala.reflect.ClassTag") diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index c9d856034237..eaac50002b94 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -1778,10 +1778,14 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling // then the symbol referred to in the subtype must have a signature that coincides // in its parameters with the refinement's signature. The reason for the check // is that if the refinement does not refer to a member symbol, we will have to - // resort to reflection to invoke the member. And reflection needs to know exact - // erased parameter types. See neg/i12211.scala. + // resort to reflection to invoke the member. And Java reflection needs to know exact + // erased parameter types. See neg/i12211.scala. Other reflection algorithms could + // conceivably dispatch without knowning precise parameter signatures. One can signal + // this by inheriting from the `scala.reflect.SignatureCanBeImprecise` marker trait, + // in which case the signature test is elided. def sigsOK(symInfo: Type, info2: Type) = tp2.underlyingClassRef(refinementOK = true).member(name).exists + || tp2.derivesFrom(defn.WithoutPreciseParameterTypesClass) || symInfo.isInstanceOf[MethodType] && symInfo.signature.consistentParams(info2.signature) diff --git a/docs/docs/reference/changed-features/structural-types-spec.md b/docs/docs/reference/changed-features/structural-types-spec.md index e678cb8d4edf..391d91f92a13 100644 --- a/docs/docs/reference/changed-features/structural-types-spec.md +++ b/docs/docs/reference/changed-features/structural-types-spec.md @@ -100,12 +100,19 @@ conversion that can turn `v` into a `Selectable`, and the selection methods coul val b: { def put(x: String): Unit } = a // error b.put("abc") // looks for a method with a `String` parameter ``` - The second to last line is not well-typed, since the erasure of the parameter type of `put` in class `Sink` is `Object`, but the erasure of `put`'s parameter in the type of `b` is `String`. This additional condition is necessary, since we will have to resort to reflection to call a structural member like `put` in the type of `b` above. The condition ensures that the statically known parameter types of the refinement correspond up to erasure to the parameter types of the selected call target at runtime. - - The usual reflection dispatch algorithms need to know exact erased parameter types. For instance, if the example above would typecheck, the call + The second to last line is not well-typed, + since the erasure of the parameter type of `put` in class `Sink` is `Object`, + but the erasure of `put`'s parameter in the type of `b` is `String`. + This additional condition is necessary, since we will have to resort + to some (as yet unknown) form of reflection to call a structural member + like `put` in the type of `b` above. The condition ensures that the statically + known parameter types of the refinement correspond up to erasure to the + parameter types of the selected call target at runtime. + + Most reflection dispatch algorithms need to know exact erased parameter types. For instance, if the example above would typecheck, the call `b.put("abc")` on the last line would look for a method `put` in the runtime type of `b` that takes a `String` parameter. But the `put` method is the one from class `Sink`, which takes an `Object` parameter. Hence the call would fail at runtime with a `NoSuchMethodException`. - One might hope for a "more intelligent" reflexive dispatch algorithm that does not require exact parameter type matching. Unfortunately, this can always run into ambiguities. For instance, continuing the example above, we might introduce a new subclass `Sink1` of `Sink` and change the definition of `a` as follows: + One might hope for a "more intelligent" reflexive dispatch algorithm that does not require exact parameter type matching. Unfortunately, this can always run into ambiguities, as long as overloading is a possibility. For instance, continuing the example above, we might introduce a new subclass `Sink1` of `Sink` and change the definition of `a` as follows: ```scala class Sink1[A] extends Sink[A] { def put(x: "123") = ??? } @@ -116,6 +123,20 @@ conversion that can turn `v` into a `Selectable`, and the selection methods coul types `Object` and `String`, respectively. Yet dynamic dispatch still needs to go to the first `put` method, even though the second looks like a better match. + For the cases where we can in fact implement reflection without knowing precise parameter types (for instance if static overloading is replaced by dynamically dispatched multi-methods), there is an escape hatch. For types that extend `scala.Selectable.WithoutPreciseParameterTypes` the signature check is omitted. Example: + + ```scala + trait MultiMethodSelectable extends Selectable.WithoutPreciseParameterTypes: + // Assume this version of `applyDynamic` can be implemented without knowing + // precise parameter types `paramTypes`: + def applyDynamic(name: String, paramTypes: Class[_]*)(args: Any*): Any = ??? + + class Sink[A] extends MultiMethodSelectable: + def put(x: A): Unit = {} + + val a = new Sink[String] + val b: MultiMethodSelectable { def put(x: String): Unit } = a // OK + ``` ## Differences with Scala 2 Structural Types - Scala 2 supports structural types by means of Java reflection. Unlike diff --git a/library/src/scala/Selectable.scala b/library/src/scala/Selectable.scala index f89102ae4802..33b839f71b30 100644 --- a/library/src/scala/Selectable.scala +++ b/library/src/scala/Selectable.scala @@ -1,5 +1,7 @@ package scala +import scala.annotation.experimental + /** A marker trait for objects that support structural selection via * `selectDynamic` and `applyDynamic` * @@ -34,3 +36,19 @@ object Selectable: implicit def reflectiveSelectableFromLangReflectiveCalls(x: Any)( using scala.languageFeature.reflectiveCalls): scala.reflect.Selectable = scala.reflect.Selectable.reflectiveSelectable(x) + + /** A marker trait for subclasses of `Selectable` indicating + * that precise parameter types are not needed for method dispatch. That is, + * a class inheriting from this trait and implementing + * + * def applyDynamic(name: String, paramTypes: Class[_]*)(args: Any*) + * + * should dispatch to a method with the given `name` without having to rely + * on the precise `paramTypes`. Subtypes of `WithoutPreciseParameterTypes` + * can have more relaxed subtyping rules for refinements. They do not need + * the additional restriction that the signatures of the refinement and + * the definition that implements the refinment must match. + */ + @experimental + trait WithoutPreciseParameterTypes extends Selectable +end Selectable diff --git a/library/src/scala/reflect/Selectable.scala b/library/src/scala/reflect/Selectable.scala index a9606fd7b45b..0286eba5a35e 100644 --- a/library/src/scala/reflect/Selectable.scala +++ b/library/src/scala/reflect/Selectable.scala @@ -49,3 +49,4 @@ object Selectable: @inline // important for Scala.js private final class DefaultSelectable(override protected val selectedValue: Any) extends Selectable +end Selectable diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index fd833d95e249..6b9633481553 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -27,5 +27,6 @@ object MiMaFilters { exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#SymbolMethods.typeMember"), exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#SymbolMethods.typeMembers"), exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#TermParamClauseMethods.isErased"), + exclude[MissingClassProblem]("scala.Selectable$WithoutPreciseParameterTypes") ) } diff --git a/tests/pos/i12211.scala b/tests/pos/i12211.scala index a8ddbc158e3c..ffd2fd95f3f3 100644 --- a/tests/pos/i12211.scala +++ b/tests/pos/i12211.scala @@ -19,3 +19,14 @@ class BB[T] def test3: (a: AA) => (b: BB[a.type]) => BB[?] = (a: AA) => (b: BB[a.type]) => b + +@annotation.experimental // TODO: Remove once WithoutPreciseParameterTypes is no longer experimental +trait RelaxedSelectable extends Selectable.WithoutPreciseParameterTypes: + def applyDynamic(name: String, paramTypes: Class[_]*)(args: Any*): Any = ??? +@annotation.experimental // TODO: Remove once WithoutPreciseParameterTypes is no longer experimental +class Sink[A] extends RelaxedSelectable { + def put(x: A): Unit = {} +} +val a = new Sink[String] +val b: RelaxedSelectable { def put(x: String): Unit } = a +val _ = b.put("")