Skip to content

Implement structural type member access #1881

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Feb 1, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions compiler/src/dotty/tools/dotc/ast/TreeInfo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,28 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] =>
case nil =>
Nil
}

/** Is this a selection of a member of a structural type that is not a member
* of an underlying class or trait?
*/
def isStructuralTermSelect(tree: Tree)(implicit ctx: Context) = tree match {
case tree: Select =>
def hasRefinement(qualtpe: Type): Boolean = qualtpe.dealias match {
case RefinedType(parent, rname, rinfo) =>
rname == tree.name || hasRefinement(parent)
case tp: TypeProxy =>
hasRefinement(tp.underlying)
case tp: OrType =>
hasRefinement(tp.tp1) || hasRefinement(tp.tp2)
Copy link
Member

@smarter smarter Jan 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aren't we mixing up the body of OrType and AndType here? To select foo, foo needs to be present in both sides of an OrType, but only one side of an AndType. In any case, we need some testcases for structural type selection on union/intersection types

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It needs to be present on both sides, but one side might be a regular class member. As long as one side comes from a refinement, the access is reflection based. For an AndType, it's the other way round. A single class member on one side is sufficient for regular access.

Copy link
Member

@smarter smarter Jan 8, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see! The AndType case makes sense, the OrType case seems suspicious but I don't think it matters since we no longer allow calling a method on a union type that matches more than one symbol, e.g. the following doesn't compile:

class Closeable {
  def close(): Unit = {}
}


object Test {
  import scala.reflect.Selectable.reflectiveSelectable

  def f(closeable: (Selectable { def close(): Unit }) | Closeable) =
    closeable.close() // error: value `close` is not a member of (Selectable{close: ()Unit} | Closeable)(closeable)

  def main(args: Array[String]): Unit = {
    f(new Closeable)
  }
}

case tp: AndType =>
hasRefinement(tp.tp1) && hasRefinement(tp.tp2)
case _ =>
false
}
!tree.symbol.exists && tree.isTerm && hasRefinement(tree.qualifier.tpe)
case _ =>
false
}
}

object TreeInfo {
Expand Down
4 changes: 4 additions & 0 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ object Definitions {
* else without affecting the set of programs that can be compiled.
*/
val MaxImplementedFunctionArity = 22

/** The maximal arity of a function that can be accessed as member of a structural type */
val MaxStructuralMethodArity = 7
}

/** A class defining symbols and types of standard definitions
Expand Down Expand Up @@ -505,6 +508,7 @@ class Definitions {
lazy val LanguageModuleRef = ctx.requiredModule("scala.language")
def LanguageModuleClass(implicit ctx: Context) = LanguageModuleRef.symbol.moduleClass.asClass
lazy val NonLocalReturnControlType: TypeRef = ctx.requiredClassRef("scala.runtime.NonLocalReturnControl")
lazy val SelectableType: TypeRef = ctx.requiredClassRef("scala.Selectable")

lazy val ClassTagType = ctx.requiredClassRef("scala.reflect.ClassTag")
def ClassTagClass(implicit ctx: Context) = ClassTagType.symbol.asClass
Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/core/StdNames.scala
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,7 @@ object StdNames {
val sameElements: N = "sameElements"
val scala_ : N = "scala"
val selectDynamic: N = "selectDynamic"
val selectDynamicMethod: N = "selectDynamicMethod"
val selectOverloadedMethod: N = "selectOverloadedMethod"
val selectTerm: N = "selectTerm"
val selectType: N = "selectType"
Expand Down
66 changes: 64 additions & 2 deletions compiler/src/dotty/tools/dotc/typer/Dynamic.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,35 @@ import dotty.tools.dotc.ast.tpd
import dotty.tools.dotc.ast.untpd
import dotty.tools.dotc.core.Constants.Constant
import dotty.tools.dotc.core.Contexts.Context
import dotty.tools.dotc.core.Names.Name
import dotty.tools.dotc.core.Names.{Name, TermName}
import dotty.tools.dotc.core.StdNames._
import dotty.tools.dotc.core.Types._
import dotty.tools.dotc.core.Decorators._
import core.Symbols._
import core.Definitions
import Inferencing._
import ErrorReporting._

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

/** Translates selection that does not typecheck according to the scala.Dynamic rules:
/** Handles programmable member selections of `Dynamic` instances and values
* with structural types. Two functionalities:
*
* 1. Translates selection that does not typecheck according to the scala.Dynamic rules:
* foo.bar(baz) = quux ~~> foo.selectDynamic(bar).update(baz, quux)
* foo.bar = baz ~~> foo.updateDynamic("bar")(baz)
* foo.bar(x = bazX, y = bazY, baz, ...) ~~> foo.applyDynamicNamed("bar")(("x", bazX), ("y", bazY), ("", baz), ...)
* foo.bar(baz0, baz1, ...) ~~> foo.applyDynamic(bar)(baz0, baz1, ...)
* foo.bar ~~> foo.selectDynamic(bar)
*
* The first matching rule of is applied.
*
* 2. Translates member selections on structural types to calls of `selectDynamic`
* or `selectDynamicMethod` on a `Selectable` instance. @See handleStructural.
*
*/
trait Dynamic { self: Typer with Applications =>
import Dynamic._
Expand Down Expand Up @@ -100,4 +110,56 @@ trait Dynamic { self: Typer with Applications =>
else untpd.TypeApply(select, targs)
untpd.Apply(selectWithTypes, Literal(Constant(name.toString)))
}

/** Handle reflection-based dispatch for members of structural types.
* Given `x.a`, where `x` is of (widened) type `T` and `x.a` is of type `U`:
*
* If `U` is a value type, map `x.a` to the equivalent of:
*
* (x: Selectable).selectDynamic(x, "a").asInstanceOf[U]
*
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parameter to selectDynamic is incorrect, should be selectDynamic("a").

* If `U` is a method type (T1,...,Tn)R, map `x.a` to the equivalent of:
*
* (x: Selectable).selectDynamicMethod("a", CT1, ..., CTn).asInstanceOf[(T1,...,Tn) => R]
*
* where CT1,...,CTn are the class tags representing the erasure of T1,...,Tn.
*
* It's an error if U is neither a value nor a method type, or a dependent method
* type, or of too large arity (limit is Definitions.MaxStructuralMethodArity).
*/
def handleStructural(tree: Tree)(implicit ctx: Context): Tree = {
val Select(qual, name) = tree

def structuralCall(selectorName: TermName, formals: List[Tree]) = {
val selectable = adapt(qual, defn.SelectableType)
val scall = untpd.Apply(
untpd.TypedSplice(selectable.select(selectorName)),
(Literal(Constant(name.toString)) :: formals).map(untpd.TypedSplice(_)))
typed(scall)
}

def fail(reason: String) =
errorTree(tree, em"Structural access not allowed on method $name because it $reason")

tree.tpe.widen match {
case tpe: MethodType =>
if (tpe.isDependent)
fail(i"has a dependent method type")
else if (tpe.paramNames.length > Definitions.MaxStructuralMethodArity)
fail(i"""takes too many parameters.
|Structural types only support methods taking up to ${Definitions.MaxStructuralMethodArity} arguments""")
else {
def issueError(msgFn: String => String): Unit = ctx.error(msgFn(""), tree.pos)
val ctags = tpe.paramTypes.map(pt =>
inferImplicitArg(defn.ClassTagType.appliedTo(pt :: Nil), issueError, tree.pos.endPos))
structuralCall(nme.selectDynamicMethod, ctags).asInstance(tpe.toFunctionType())
}
case tpe: ValueType =>
structuralCall(nme.selectDynamic, Nil).asInstance(tpe)
case tpe: PolyType =>
fail("is polymorphic")
case tpe =>
fail(i"has an unsupported type: $tpe")
}
}
}
8 changes: 4 additions & 4 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1040,9 +1040,8 @@ class Typer extends Namer with TypeAssigner with Applications with Implicits wit
typr.println(s"adding refinement $refinement")
checkRefinementNonCyclic(refinement, refineCls, seen)
val rsym = refinement.symbol
if (rsym.is(Method) && rsym.allOverriddenSymbols.isEmpty)
ctx.error(i"refinement $rsym without matching type in parent $tpt1", refinement.pos)
}
if (rsym.info.isInstanceOf[PolyType] && rsym.allOverriddenSymbols.isEmpty)
ctx.error(i"polymorphic refinement $rsym without matching type in parent $tpt1 is no longer allowed", refinement.pos) }
assignType(cpy.RefinedTypeTree(tree)(tpt1, refinements1), tpt1, refinements1, refineCls)
}

Expand Down Expand Up @@ -2045,7 +2044,8 @@ class Typer extends Namer with TypeAssigner with Applications with Implicits wit
adaptInterpolated(tree.appliedToTypeTrees(typeArgs), pt, original))
}
case wtp =>
pt match {
if (isStructuralTermSelect(tree)) adapt(handleStructural(tree), pt)
else pt match {
case pt: FunProto =>
adaptToArgs(wtp, pt)
case pt: PolyProto =>
Expand Down
8 changes: 8 additions & 0 deletions library/src/scala/Selectable.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package scala
import scala.reflect.ClassTag

trait Selectable extends Any {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to have following annotation:

@implicitNotFound("no Selectable instance found to implement reflective access to structural type ${T}. Forget to import scala.reflect.Selectable.reflectiveSelectable?")

def selectDynamic(name: String): Any
def selectDynamicMethod(name: String, paramClasses: ClassTag[_]*): Any =
new UnsupportedOperationException("selectDynamicMethod")
}
73 changes: 73 additions & 0 deletions library/src/scala/reflect/Selectable.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package scala.reflect

class Selectable(val receiver: Any) extends AnyVal with scala.Selectable {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would call this ReflectiveSelectable or something like that, it's easy to miss the prefix.

def selectDynamic(name: String): Any = {
val rcls = receiver.getClass
try {
val fld = rcls.getField(name)
fld.get(receiver)
}
catch {
case ex: NoSuchFieldError =>
selectDynamicMethod(name).asInstanceOf[() => Any]()
}
}

override def selectDynamicMethod(name: String, paramTypes: ClassTag[_]*): Any = {
val rcls = receiver.getClass
val paramClasses = paramTypes.map(_.runtimeClass)
val mth = rcls.getMethod(name, paramClasses: _*)
paramTypes.length match {
case 0 => () =>
mth.invoke(receiver)
case 1 => (x0: Any) =>
mth.invoke(receiver, x0.asInstanceOf[Object])
case 2 => (x0: Any, x1: Any) =>
mth.invoke(receiver,
x0.asInstanceOf[Object],
x1.asInstanceOf[Object])
case 3 => (x0: Any, x1: Any, x2: Any) =>
mth.invoke(receiver,
x0.asInstanceOf[Object],
x1.asInstanceOf[Object],
x2.asInstanceOf[Object])
case 4 => (x0: Any, x1: Any, x2: Any, x3: Any) =>
mth.invoke(receiver,
x0.asInstanceOf[Object],
x1.asInstanceOf[Object],
x2.asInstanceOf[Object],
x3.asInstanceOf[Object])
case 5 => (x0: Any, x1: Any, x2: Any, x3: Any, x4: Any) =>
mth.invoke(receiver,
x0.asInstanceOf[Object],
x1.asInstanceOf[Object],
x2.asInstanceOf[Object],
x3.asInstanceOf[Object],
x4.asInstanceOf[Object])
case 6 => (x0: Any, x1: Any, x2: Any, x3: Any, x4: Any, x5: Any) =>
mth.invoke(receiver,
x0.asInstanceOf[Object],
x1.asInstanceOf[Object],
x2.asInstanceOf[Object],
x3.asInstanceOf[Object],
x4.asInstanceOf[Object],
x5.asInstanceOf[Object])
case 7 => (x0: Any, x1: Any, x2: Any, x3: Any, x4: Any, x5: Any, x6: Any) =>
mth.invoke(receiver,
x0.asInstanceOf[Object],
x1.asInstanceOf[Object],
x2.asInstanceOf[Object],
x3.asInstanceOf[Object],
x4.asInstanceOf[Object],
x5.asInstanceOf[Object],
x6.asInstanceOf[Object])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we require functions with fixed sizes? Could we not return an object with an apply(args: Object*) and then pass the args directly to the invoke?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current solution has the advantage that the result of a Selectable is a drop in replacement of the original call. If we bunched arguments in an array, this would mean we also have the change the calling context. So it's more complicated. Selectable has lots of restrictions anyway, so one more does not really matter.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok.

Other related question: is the limit of 7 parameters an arbitrary limit or is there some factor that influenced this limit?

}
}
}

object Selectable {
implicit def reflectiveSelectable(receiver: Any): scala.Selectable = receiver match {
case receiver: scala.Selectable => receiver
case _ => new Selectable(receiver)
}
}
11 changes: 11 additions & 0 deletions tests/neg/structural.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
object Test3 {
import scala.reflect.Selectable.reflectiveSelectable
def g(x: { type T ; def t: T ; def f(a: T): Boolean }) = x.f(x.t) // error: no ClassTag for x.T
g(new { type T = Int; def t = 4; def f(a:T) = true })
g(new { type T = Any; def t = 4; def f(a:T) = true })
val y: { type T = Int; def t = 4; def f(a:T) = true }
= new { type T = Int; def t = 4; def f(a:T) = true }

def h(x: { def f[T](a: T): Int }) = x.f[Int](4) // error: polymorphic refinement method ... no longer allowed

}
14 changes: 7 additions & 7 deletions tests/neg/zoo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@ type Grass = {
}
type Animal = {
type Food
def eats(food: Food): Unit // error
def gets: Food // error
def eats(food: Food): Unit
def gets: Food
}
type Cow = {
type IsMeat = Any
type Food <: Grass
def eats(food: Grass): Unit // error
def gets: Grass // error
def eats(food: Grass): Unit
def gets: Grass
}
type Lion = {
type Food = Meat
def eats(food: Meat): Unit // error
def gets: Meat // error
def eats(food: Meat): Unit
def gets: Meat
}
def newMeat: Meat = new {
type IsMeat = Any
Expand All @@ -40,5 +40,5 @@ def newLion: Lion = new {
}
val milka = newCow
val leo = newLion
leo.eats(milka) // structural select not supported
leo.eats(milka) // error: no projector found
}
5 changes: 5 additions & 0 deletions tests/pos/i1866.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import scala.reflect.Selectable.reflectiveSelectable
object Test {
def f(g: { val update: Unit }) = g.update
def main(update: Array[String]) = {}
}
45 changes: 45 additions & 0 deletions tests/pos/zoo2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import scala.reflect.Selectable.reflectiveSelectable
object Test {
type Meat = {
type IsMeat = Any
}
type Grass = {
type IsGrass = Any
}
type Animal = {
type Food
def eats(food: Food): Unit
def gets: Food
}
type Cow = {
type IsMeat = Any
type Food <: Grass
def eats(food: Grass): Unit
def gets: Grass
}
type Lion = {
type Food = Meat
def eats(food: Meat): Unit
def gets: Meat
}
def newMeat: Meat = new {
type IsMeat = Any
}
def newGrass: Grass = new {
type IsGrass = Any
}
def newCow: Cow = new {
type IsMeat = Any
type Food = Grass
def eats(food: Grass) = ()
def gets = newGrass
}
def newLion: Lion = new {
type Food = Meat
def eats(food: Meat) = ()
def gets = newMeat
}
val milka = newCow
val leo = newLion
leo.eats(milka)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test error suggests that import scala.reflect.Projector.reflectiveProjector is missing in this test.

25 changes: 25 additions & 0 deletions tests/run/structural.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
case class Record(elems: (String, Any)*) extends Selectable {
def selectDynamic(name: String): Any = elems.find(_._1 == name).get._2
}

object Test {
import scala.reflect.Selectable.reflectiveSelectable

def f(closeable: { def close(): Unit }) =
closeable.close()

type RN = Record { val name: String; val age: Int }

def g(r: RN) = r.name

val rr: RN = Record("name" -> "Bob", "age" -> 42).asInstanceOf[RN]

def main(args: Array[String]): Unit = {
f(new java.io.PrintStream("foo"))
assert(g(rr) == "Bob")

val s: { def concat(s: String): String } = "abc"
assert(s.concat("def") == "abcdef")
}
}

1 change: 1 addition & 0 deletions tests/run/structuralNoSuchMethod.check
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
no such method
Loading