Skip to content

Commit af7fdb3

Browse files
authored
Merge pull request #1881 from dotty-staging/add-structural-select
Implement structural type member access
2 parents bb2e99c + 678e8e4 commit af7fdb3

14 files changed

+293
-13
lines changed

compiler/src/dotty/tools/dotc/ast/TreeInfo.scala

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,28 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] =>
629629
case nil =>
630630
Nil
631631
}
632+
633+
/** Is this a selection of a member of a structural type that is not a member
634+
* of an underlying class or trait?
635+
*/
636+
def isStructuralTermSelect(tree: Tree)(implicit ctx: Context) = tree match {
637+
case tree: Select =>
638+
def hasRefinement(qualtpe: Type): Boolean = qualtpe.dealias match {
639+
case RefinedType(parent, rname, rinfo) =>
640+
rname == tree.name || hasRefinement(parent)
641+
case tp: TypeProxy =>
642+
hasRefinement(tp.underlying)
643+
case tp: OrType =>
644+
hasRefinement(tp.tp1) || hasRefinement(tp.tp2)
645+
case tp: AndType =>
646+
hasRefinement(tp.tp1) && hasRefinement(tp.tp2)
647+
case _ =>
648+
false
649+
}
650+
!tree.symbol.exists && tree.isTerm && hasRefinement(tree.qualifier.tpe)
651+
case _ =>
652+
false
653+
}
632654
}
633655

634656
object TreeInfo {

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ object Definitions {
2626
* else without affecting the set of programs that can be compiled.
2727
*/
2828
val MaxImplementedFunctionArity = 22
29+
30+
/** The maximal arity of a function that can be accessed as member of a structural type */
31+
val MaxStructuralMethodArity = 7
2932
}
3033

3134
/** A class defining symbols and types of standard definitions
@@ -515,6 +518,7 @@ class Definitions {
515518
lazy val LanguageModuleRef = ctx.requiredModule("scala.language")
516519
def LanguageModuleClass(implicit ctx: Context) = LanguageModuleRef.symbol.moduleClass.asClass
517520
lazy val NonLocalReturnControlType: TypeRef = ctx.requiredClassRef("scala.runtime.NonLocalReturnControl")
521+
lazy val SelectableType: TypeRef = ctx.requiredClassRef("scala.Selectable")
518522

519523
lazy val ClassTagType = ctx.requiredClassRef("scala.reflect.ClassTag")
520524
def ClassTagClass(implicit ctx: Context) = ClassTagType.symbol.asClass

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,7 @@ object StdNames {
481481
val sameElements: N = "sameElements"
482482
val scala_ : N = "scala"
483483
val selectDynamic: N = "selectDynamic"
484+
val selectDynamicMethod: N = "selectDynamicMethod"
484485
val selectOverloadedMethod: N = "selectOverloadedMethod"
485486
val selectTerm: N = "selectTerm"
486487
val selectType: N = "selectType"

compiler/src/dotty/tools/dotc/typer/Dynamic.scala

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,35 @@ import dotty.tools.dotc.ast.tpd
77
import dotty.tools.dotc.ast.untpd
88
import dotty.tools.dotc.core.Constants.Constant
99
import dotty.tools.dotc.core.Contexts.Context
10-
import dotty.tools.dotc.core.Names.Name
10+
import dotty.tools.dotc.core.Names.{Name, TermName}
1111
import dotty.tools.dotc.core.StdNames._
1212
import dotty.tools.dotc.core.Types._
1313
import dotty.tools.dotc.core.Decorators._
14+
import core.Symbols._
15+
import core.Definitions
16+
import Inferencing._
1417
import ErrorReporting._
1518

1619
object Dynamic {
1720
def isDynamicMethod(name: Name): Boolean =
1821
name == nme.applyDynamic || name == nme.selectDynamic || name == nme.updateDynamic || name == nme.applyDynamicNamed
1922
}
2023

21-
/** Translates selection that does not typecheck according to the scala.Dynamic rules:
24+
/** Handles programmable member selections of `Dynamic` instances and values
25+
* with structural types. Two functionalities:
26+
*
27+
* 1. Translates selection that does not typecheck according to the scala.Dynamic rules:
2228
* foo.bar(baz) = quux ~~> foo.selectDynamic(bar).update(baz, quux)
2329
* foo.bar = baz ~~> foo.updateDynamic("bar")(baz)
2430
* foo.bar(x = bazX, y = bazY, baz, ...) ~~> foo.applyDynamicNamed("bar")(("x", bazX), ("y", bazY), ("", baz), ...)
2531
* foo.bar(baz0, baz1, ...) ~~> foo.applyDynamic(bar)(baz0, baz1, ...)
2632
* foo.bar ~~> foo.selectDynamic(bar)
2733
*
2834
* The first matching rule of is applied.
35+
*
36+
* 2. Translates member selections on structural types to calls of `selectDynamic`
37+
* or `selectDynamicMethod` on a `Selectable` instance. @See handleStructural.
38+
*
2939
*/
3040
trait Dynamic { self: Typer with Applications =>
3141
import Dynamic._
@@ -100,4 +110,56 @@ trait Dynamic { self: Typer with Applications =>
100110
else untpd.TypeApply(select, targs)
101111
untpd.Apply(selectWithTypes, Literal(Constant(name.toString)))
102112
}
113+
114+
/** Handle reflection-based dispatch for members of structural types.
115+
* Given `x.a`, where `x` is of (widened) type `T` and `x.a` is of type `U`:
116+
*
117+
* If `U` is a value type, map `x.a` to the equivalent of:
118+
*
119+
* (x: Selectable).selectDynamic(x, "a").asInstanceOf[U]
120+
*
121+
* If `U` is a method type (T1,...,Tn)R, map `x.a` to the equivalent of:
122+
*
123+
* (x: Selectable).selectDynamicMethod("a", CT1, ..., CTn).asInstanceOf[(T1,...,Tn) => R]
124+
*
125+
* where CT1,...,CTn are the class tags representing the erasure of T1,...,Tn.
126+
*
127+
* It's an error if U is neither a value nor a method type, or a dependent method
128+
* type, or of too large arity (limit is Definitions.MaxStructuralMethodArity).
129+
*/
130+
def handleStructural(tree: Tree)(implicit ctx: Context): Tree = {
131+
val Select(qual, name) = tree
132+
133+
def structuralCall(selectorName: TermName, formals: List[Tree]) = {
134+
val selectable = adapt(qual, defn.SelectableType)
135+
val scall = untpd.Apply(
136+
untpd.TypedSplice(selectable.select(selectorName)),
137+
(Literal(Constant(name.toString)) :: formals).map(untpd.TypedSplice(_)))
138+
typed(scall)
139+
}
140+
141+
def fail(reason: String) =
142+
errorTree(tree, em"Structural access not allowed on method $name because it $reason")
143+
144+
tree.tpe.widen match {
145+
case tpe: MethodType =>
146+
if (tpe.isDependent)
147+
fail(i"has a dependent method type")
148+
else if (tpe.paramNames.length > Definitions.MaxStructuralMethodArity)
149+
fail(i"""takes too many parameters.
150+
|Structural types only support methods taking up to ${Definitions.MaxStructuralMethodArity} arguments""")
151+
else {
152+
def issueError(msgFn: String => String): Unit = ctx.error(msgFn(""), tree.pos)
153+
val ctags = tpe.paramTypes.map(pt =>
154+
inferImplicitArg(defn.ClassTagType.appliedTo(pt :: Nil), issueError, tree.pos.endPos))
155+
structuralCall(nme.selectDynamicMethod, ctags).asInstance(tpe.toFunctionType())
156+
}
157+
case tpe: ValueType =>
158+
structuralCall(nme.selectDynamic, Nil).asInstance(tpe)
159+
case tpe: PolyType =>
160+
fail("is polymorphic")
161+
case tpe =>
162+
fail(i"has an unsupported type: $tpe")
163+
}
164+
}
103165
}

compiler/src/dotty/tools/dotc/typer/Typer.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,9 +1040,8 @@ class Typer extends Namer with TypeAssigner with Applications with Implicits wit
10401040
typr.println(s"adding refinement $refinement")
10411041
checkRefinementNonCyclic(refinement, refineCls, seen)
10421042
val rsym = refinement.symbol
1043-
if (rsym.is(Method) && rsym.allOverriddenSymbols.isEmpty)
1044-
ctx.error(i"refinement $rsym without matching type in parent $tpt1", refinement.pos)
1045-
}
1043+
if (rsym.info.isInstanceOf[PolyType] && rsym.allOverriddenSymbols.isEmpty)
1044+
ctx.error(i"polymorphic refinement $rsym without matching type in parent $tpt1 is no longer allowed", refinement.pos) }
10461045
assignType(cpy.RefinedTypeTree(tree)(tpt1, refinements1), tpt1, refinements1, refineCls)
10471046
}
10481047

@@ -2067,7 +2066,8 @@ class Typer extends Namer with TypeAssigner with Applications with Implicits wit
20672066
adaptInterpolated(tree.appliedToTypeTrees(typeArgs), pt, original))
20682067
}
20692068
case wtp =>
2070-
pt match {
2069+
if (isStructuralTermSelect(tree)) adapt(handleStructural(tree), pt)
2070+
else pt match {
20712071
case pt: FunProto =>
20722072
adaptToArgs(wtp, pt)
20732073
case pt: PolyProto =>

library/src/scala/Selectable.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package scala
2+
import scala.reflect.ClassTag
3+
4+
trait Selectable extends Any {
5+
def selectDynamic(name: String): Any
6+
def selectDynamicMethod(name: String, paramClasses: ClassTag[_]*): Any =
7+
new UnsupportedOperationException("selectDynamicMethod")
8+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package scala.reflect
2+
3+
class Selectable(val receiver: Any) extends AnyVal with scala.Selectable {
4+
def selectDynamic(name: String): Any = {
5+
val rcls = receiver.getClass
6+
try {
7+
val fld = rcls.getField(name)
8+
fld.get(receiver)
9+
}
10+
catch {
11+
case ex: NoSuchFieldError =>
12+
selectDynamicMethod(name).asInstanceOf[() => Any]()
13+
}
14+
}
15+
16+
override def selectDynamicMethod(name: String, paramTypes: ClassTag[_]*): Any = {
17+
val rcls = receiver.getClass
18+
val paramClasses = paramTypes.map(_.runtimeClass)
19+
val mth = rcls.getMethod(name, paramClasses: _*)
20+
paramTypes.length match {
21+
case 0 => () =>
22+
mth.invoke(receiver)
23+
case 1 => (x0: Any) =>
24+
mth.invoke(receiver, x0.asInstanceOf[Object])
25+
case 2 => (x0: Any, x1: Any) =>
26+
mth.invoke(receiver,
27+
x0.asInstanceOf[Object],
28+
x1.asInstanceOf[Object])
29+
case 3 => (x0: Any, x1: Any, x2: Any) =>
30+
mth.invoke(receiver,
31+
x0.asInstanceOf[Object],
32+
x1.asInstanceOf[Object],
33+
x2.asInstanceOf[Object])
34+
case 4 => (x0: Any, x1: Any, x2: Any, x3: Any) =>
35+
mth.invoke(receiver,
36+
x0.asInstanceOf[Object],
37+
x1.asInstanceOf[Object],
38+
x2.asInstanceOf[Object],
39+
x3.asInstanceOf[Object])
40+
case 5 => (x0: Any, x1: Any, x2: Any, x3: Any, x4: Any) =>
41+
mth.invoke(receiver,
42+
x0.asInstanceOf[Object],
43+
x1.asInstanceOf[Object],
44+
x2.asInstanceOf[Object],
45+
x3.asInstanceOf[Object],
46+
x4.asInstanceOf[Object])
47+
case 6 => (x0: Any, x1: Any, x2: Any, x3: Any, x4: Any, x5: Any) =>
48+
mth.invoke(receiver,
49+
x0.asInstanceOf[Object],
50+
x1.asInstanceOf[Object],
51+
x2.asInstanceOf[Object],
52+
x3.asInstanceOf[Object],
53+
x4.asInstanceOf[Object],
54+
x5.asInstanceOf[Object])
55+
case 7 => (x0: Any, x1: Any, x2: Any, x3: Any, x4: Any, x5: Any, x6: Any) =>
56+
mth.invoke(receiver,
57+
x0.asInstanceOf[Object],
58+
x1.asInstanceOf[Object],
59+
x2.asInstanceOf[Object],
60+
x3.asInstanceOf[Object],
61+
x4.asInstanceOf[Object],
62+
x5.asInstanceOf[Object],
63+
x6.asInstanceOf[Object])
64+
}
65+
}
66+
}
67+
68+
object Selectable {
69+
implicit def reflectiveSelectable(receiver: Any): scala.Selectable = receiver match {
70+
case receiver: scala.Selectable => receiver
71+
case _ => new Selectable(receiver)
72+
}
73+
}

tests/neg/structural.scala

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
object Test3 {
2+
import scala.reflect.Selectable.reflectiveSelectable
3+
def g(x: { type T ; def t: T ; def f(a: T): Boolean }) = x.f(x.t) // error: no ClassTag for x.T
4+
g(new { type T = Int; def t = 4; def f(a:T) = true })
5+
g(new { type T = Any; def t = 4; def f(a:T) = true })
6+
val y: { type T = Int; def t = 4; def f(a:T) = true }
7+
= new { type T = Int; def t = 4; def f(a:T) = true }
8+
9+
def h(x: { def f[T](a: T): Int }) = x.f[Int](4) // error: polymorphic refinement method ... no longer allowed
10+
11+
}

tests/neg/zoo.scala

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,19 @@ type Grass = {
77
}
88
type Animal = {
99
type Food
10-
def eats(food: Food): Unit // error
11-
def gets: Food // error
10+
def eats(food: Food): Unit
11+
def gets: Food
1212
}
1313
type Cow = {
1414
type IsMeat = Any
1515
type Food <: Grass
16-
def eats(food: Grass): Unit // error
17-
def gets: Grass // error
16+
def eats(food: Grass): Unit
17+
def gets: Grass
1818
}
1919
type Lion = {
2020
type Food = Meat
21-
def eats(food: Meat): Unit // error
22-
def gets: Meat // error
21+
def eats(food: Meat): Unit
22+
def gets: Meat
2323
}
2424
def newMeat: Meat = new {
2525
type IsMeat = Any
@@ -40,5 +40,5 @@ def newLion: Lion = new {
4040
}
4141
val milka = newCow
4242
val leo = newLion
43-
leo.eats(milka) // structural select not supported
43+
leo.eats(milka) // error: no projector found
4444
}

tests/pos/i1866.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import scala.reflect.Selectable.reflectiveSelectable
2+
object Test {
3+
def f(g: { val update: Unit }) = g.update
4+
def main(update: Array[String]) = {}
5+
}

tests/pos/zoo2.scala

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import scala.reflect.Selectable.reflectiveSelectable
2+
object Test {
3+
type Meat = {
4+
type IsMeat = Any
5+
}
6+
type Grass = {
7+
type IsGrass = Any
8+
}
9+
type Animal = {
10+
type Food
11+
def eats(food: Food): Unit
12+
def gets: Food
13+
}
14+
type Cow = {
15+
type IsMeat = Any
16+
type Food <: Grass
17+
def eats(food: Grass): Unit
18+
def gets: Grass
19+
}
20+
type Lion = {
21+
type Food = Meat
22+
def eats(food: Meat): Unit
23+
def gets: Meat
24+
}
25+
def newMeat: Meat = new {
26+
type IsMeat = Any
27+
}
28+
def newGrass: Grass = new {
29+
type IsGrass = Any
30+
}
31+
def newCow: Cow = new {
32+
type IsMeat = Any
33+
type Food = Grass
34+
def eats(food: Grass) = ()
35+
def gets = newGrass
36+
}
37+
def newLion: Lion = new {
38+
type Food = Meat
39+
def eats(food: Meat) = ()
40+
def gets = newMeat
41+
}
42+
val milka = newCow
43+
val leo = newLion
44+
leo.eats(milka)
45+
}

tests/run/structural.scala

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
case class Record(elems: (String, Any)*) extends Selectable {
2+
def selectDynamic(name: String): Any = elems.find(_._1 == name).get._2
3+
}
4+
5+
object Test {
6+
import scala.reflect.Selectable.reflectiveSelectable
7+
8+
def f(closeable: { def close(): Unit }) =
9+
closeable.close()
10+
11+
type RN = Record { val name: String; val age: Int }
12+
13+
def g(r: RN) = r.name
14+
15+
val rr: RN = Record("name" -> "Bob", "age" -> 42).asInstanceOf[RN]
16+
17+
def main(args: Array[String]): Unit = {
18+
f(new java.io.PrintStream("foo"))
19+
assert(g(rr) == "Bob")
20+
21+
val s: { def concat(s: String): String } = "abc"
22+
assert(s.concat("def") == "abcdef")
23+
}
24+
}
25+
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
no such method

0 commit comments

Comments
 (0)