-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Changes from 2 commits
aa6ebe9
7e3f69a
69feaa8
bb81d5d
8464c16
2bbf9ca
678e8e4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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._ | ||
|
@@ -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] | ||
* | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The parameter to |
||
* 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") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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") | ||
} | ||
} | ||
} |
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") | ||
} |
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 | ||
} |
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) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The test error suggests that |
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") | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
no such method |
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") | ||
} | ||
|
||
} |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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
andAndType
here? To selectfoo
,foo
needs to be present in both sides of anOrType
, but only one side of anAndType
. In any case, we need some testcases for structural type selection on union/intersection typesThere was a problem hiding this comment.
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.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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, theOrType
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: