Skip to content

Commit 3e6c579

Browse files
committed
Add escape hatch Selectable.WithoutPreciseParameterTypes
1 parent 92c7ae7 commit 3e6c579

File tree

6 files changed

+50
-5
lines changed

6 files changed

+50
-5
lines changed

compiler/src/dotty/tools/dotc/core/Definitions.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -760,6 +760,8 @@ class Definitions {
760760
@tu lazy val LanguageDeprecatedModule: Symbol = requiredModule("scala.language.deprecated")
761761
@tu lazy val NonLocalReturnControlClass: ClassSymbol = requiredClass("scala.runtime.NonLocalReturnControl")
762762
@tu lazy val SelectableClass: ClassSymbol = requiredClass("scala.Selectable")
763+
@tu lazy val WithoutPreciseParameterTypesClass: Symbol = getClassIfDefined("scala.Selectable.WithoutPreciseParameterTypes")
764+
// todo: make this a required class from 3.1 on
763765

764766
@tu lazy val ReflectPackageClass: Symbol = requiredPackage("scala.reflect.package").moduleClass
765767
@tu lazy val ClassTagClass: ClassSymbol = requiredClass("scala.reflect.ClassTag")

compiler/src/dotty/tools/dotc/core/TypeComparer.scala

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1730,10 +1730,14 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
17301730
// then the symbol referred to in the subtype must have a signature that coincides
17311731
// in its parameters with the refinement's signature. The reason for the check
17321732
// is that if the refinement does not refer to a member symbol, we will have to
1733-
// resort to reflection to invoke the member. And reflection needs to know exact
1734-
// erased parameter types. See neg/i12211.scala.
1733+
// resort to reflection to invoke the member. And Java reflection needs to know exact
1734+
// erased parameter types. See neg/i12211.scala. Other reflection algorithms could
1735+
// conceivably dispatch without knowning precise parameter signatures. One can signal
1736+
// this by inheriting from the `scala.reflect.SignatureCanBeImprecise` marker trait,
1737+
// in which case the signature test is elided.
17351738
def sigsOK(symInfo: Type, info2: Type) =
17361739
tp2.underlyingClassRef(refinementOK = true).member(name).exists
1740+
|| tp2.derivesFrom(defn.WithoutPreciseParameterTypesClass)
17371741
|| symInfo.isInstanceOf[MethodType]
17381742
&& symInfo.signature.consistentParams(info2.signature)
17391743

docs/docs/reference/changed-features/structural-types-spec.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,12 @@ conversion that can turn `v` into a `Selectable`, and the selection methods coul
100100
val b: { def put(x: String): Unit } = a // error
101101
b.put("abc") // looks for a method with a `String` parameter
102102
```
103-
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 the refinement of 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.
103+
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 the refinement of 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.
104104

105-
The usual reflection dispatch algorithms need to know exact erased parameter types. For instance, if the example above would typecheck, the call
105+
Most reflection dispatch algorithms need to know exact erased parameter types. For instance, if the example above would typecheck, the call
106106
`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`.
107107

108-
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:
108+
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:
109109

110110
```scala
111111
class Sink1[A] extends Sink[A] { def put(x: "123") = ??? }
@@ -116,6 +116,20 @@ conversion that can turn `v` into a `Selectable`, and the selection methods coul
116116
types `Object` and `String`, respectively. Yet dynamic dispatch still needs to go
117117
to the first `put` method, even though the second looks like a better match.
118118

119+
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:
120+
121+
```scala
122+
trait MultiMethodSelectable extends Selectable.WithoutPreciseParameterTypes:
123+
// Assume this version of `applyDynamic` can be implemented without knowing
124+
// precise parameter types `paramTypes`:
125+
def applyDynamic(name: String, paramTypes: Class[_]*)(args: Any*): Any = ???
126+
127+
class Sink[A] extends MultiMethodSelectable:
128+
def put(x: A): Unit = {}
129+
130+
val a = new Sink[String]
131+
val b: MultiMethodSelectable { def put(x: String): Unit } = a // OK
132+
```
119133
## Differences with Scala 2 Structural Types
120134

121135
- Scala 2 supports structural types by means of Java reflection. Unlike

library/src/scala/Selectable.scala

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,18 @@ object Selectable:
3434
implicit def reflectiveSelectableFromLangReflectiveCalls(x: Any)(
3535
using scala.languageFeature.reflectiveCalls): scala.reflect.Selectable =
3636
scala.reflect.Selectable.reflectiveSelectable(x)
37+
38+
/** A marker trait for subclasses of `Selectable` indicating
39+
* that precise parameter types are not needed for method dispatch. That is,
40+
* a class inheriting from this trait and implementing
41+
*
42+
* def applyDynamic(name: String, paramTypes: Class[_]*)(args: Any*)
43+
*
44+
* should dispatch to a method with the given `name` without having to rely
45+
* on the precise `paramTypes`. Subtypes of `SignatureCanBeImprecise`
46+
* can have more relaxed subtyping rules for refinements. They do not need
47+
* the additional restriction that the signatures of the refinement and
48+
* the definition that implements the refinment must match.
49+
*/
50+
trait WithoutPreciseParameterTypes extends Selectable
51+
end Selectable

library/src/scala/reflect/Selectable.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,4 @@ object Selectable:
4949

5050
@inline // important for Scala.js
5151
private final class DefaultSelectable(override protected val selectedValue: Any) extends Selectable
52+
end Selectable

tests/pos/i12211.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,12 @@ class BB[T]
1919

2020
def test3: (a: AA) => (b: BB[a.type]) => BB[?] =
2121
(a: AA) => (b: BB[a.type]) => b
22+
23+
trait RelaxedSelectable extends Selectable.WithoutPreciseParameterTypes:
24+
def applyDynamic(name: String, paramTypes: Class[_]*)(args: Any*): Any = ???
25+
class Sink[A] extends RelaxedSelectable {
26+
def put(x: A): Unit = {}
27+
}
28+
val a = new Sink[String]
29+
val b: RelaxedSelectable { def put(x: String): Unit } = a
30+
val _ = b.put("")

0 commit comments

Comments
 (0)