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 2 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 && tree.tpe.widen <:< rinfo || 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
5 changes: 5 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 thta can be accessed as member of a structrual type */
Copy link
Member

Choose a reason for hiding this comment

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

typo: thta -> that

val MaxStructuralMethodArity = 7
}

/** A class defining symbols and types of standard definitions
Expand Down Expand Up @@ -505,6 +508,8 @@ 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 ProjectorType: TypeRef = ctx.requiredClassRef("scala.Projector")
def ProjectorClass(implicit ctx: Context) = ProjectorType.symbol.asClass

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 @@ -414,6 +414,7 @@ object StdNames {
val genericArrayOps: N = "genericArrayOps"
val get: N = "get"
val getClass_ : N = "getClass"
val getMethod : N = "getMethod"
val getOrElse: N = "getOrElse"
val hasNext: N = "hasNext"
val hashCode_ : N = "hashCode"
Expand Down
64 changes: 62 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 seclections on structural types by means of an implicit
* Projector instance. @See handleStructural.
*
*/
trait Dynamic { self: Typer with Applications =>
import Dynamic._
Expand Down Expand Up @@ -100,4 +110,54 @@ 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:
*
* implicitly[Projector[T]].get(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:
*
* implicitly[Projector[T]].getMethod(x, "a")(CT1, ..., CTn).asInstanceOf[(T1,...,Tn) => R]
*
* where CT1,...,CTn are the classtags representing the erasure of T1,...,Tn.
*
* The small print: (1) T is forced to be fully defined. (2) 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 issueError(msgFn: String => String): Unit = ctx.error(msgFn("reflective call"), tree.pos)
def implicitArg(tpe: Type) = inferImplicitArg(tpe, issueError, tree.pos.endPos)
val projector = implicitArg(defn.ProjectorType.appliedTo(qual.tpe.widen))

def structuralCall(getterName: TermName, formals: List[Tree]) = {
val scall = untpd.Apply(
untpd.TypedSplice(projector.select(getterName)),
(qual :: 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")
fullyDefinedType(tree.tpe.widen, "structural access", tree.pos) 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")
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 expand this message to say "Structural types only support methods taking up to ${Definitions.MaxStructuralMethodArity} arguments", to keep people from guessing.

val ctags = tpe.paramTypes.map(pt =>
implicitArg(defn.ClassTagType.appliedTo(pt :: Nil)))
structuralCall(nme.getMethod, ctags).asInstance(tpe.toFunctionType())
case tpe: ValueType =>
structuralCall(nme.get, Nil).asInstance(tpe)
case tpe: PolyType =>
fail("is polymorphic")
case tpe =>
fail(i"has an unsupported type: $tpe")
}
}
}
6 changes: 2 additions & 4 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1039,9 +1039,6 @@ class Typer extends Namer with TypeAssigner with Applications with Implicits wit
for (refinement <- refinements1) { // TODO: get clarity whether we want to enforce these conditions
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)
}
assignType(cpy.RefinedTypeTree(tree)(tpt1, refinements1), tpt1, refinements1, refineCls)
}
Expand Down Expand Up @@ -2045,7 +2042,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
10 changes: 10 additions & 0 deletions library/src/scala/Projector.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package scala
import scala.reflect.ClassTag
import scala.annotation.implicitNotFound

@implicitNotFound("no projector instance found to implement reflective access to structural type ${T}")
trait Projector[-T] extends Any {
def get(receiver: T, name: String): Any
def getMethod(receiver: T, name: String, paramClasses: ClassTag[_]*): Any =
new UnsupportedOperationException("getMethod")
}
71 changes: 71 additions & 0 deletions library/src/scala/reflect/Projector.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package scala.reflect

class Projector extends scala.Projector[Any] {
import Projector._
def get(receiver: Any, name: String): Any = {
val rcls = receiver.getClass
try {
val fld = rcls.getField(name)
fld.get(receiver)
}
catch {
case ex: NoSuchFieldError =>
getMethod(receiver, name).asInstanceOf[() => Any]()
}
}

override def getMethod(receiver: Any, 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])
}
}
}

object Projector {
implicit val reflectiveProjector: scala.Projector[Any] = new Projector
}
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
}
44 changes: 44 additions & 0 deletions tests/pos/zoo2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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.

33 changes: 33 additions & 0 deletions tests/run/structural.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
case class Record(elems: (String, Any)*)

object Record {

implicit def projector: Projector[Record] = new Projector[Record] {
def get(receiver: Record, name: String): Any =
receiver.elems.find(_._1 == name).get._2
}

}

object Test {
import scala.reflect.Projector.reflectiveProjector
import Record.projector

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

type RN = Record { val name: String }

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
23 changes: 23 additions & 0 deletions tests/run/structuralNoSuchMethod.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import scala.reflect.Projector.reflectiveProjector

/** Demonstrates limitation of structural method dispatch (in Scala 2.x and dotty).
* The method must be defined at exactly the argument types given in the structural type;
* Generic instantiation is not possible.
*/
object Test {
type T = { def f(x: String, y: String): String }

class C[X] {
def f(x: X, y: String): String = "f1"
}

val x: T = new C[String]

def main(args: Array[String]) =
try println(x.f("", "")) // throws NoSuchMethodException
catch {
case ex: NoSuchMethodException =>
println("no such method")
}

}