-
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 all 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 |
---|---|---|
|
@@ -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._ | ||
|
@@ -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] | ||
* | ||
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: | ||
* | ||
* (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") | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package scala | ||
import scala.reflect.ClassTag | ||
|
||
trait Selectable extends Any { | ||
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. Better to have following annotation:
|
||
def selectDynamic(name: String): Any | ||
def selectDynamicMethod(name: String, paramClasses: ClassTag[_]*): Any = | ||
new UnsupportedOperationException("selectDynamicMethod") | ||
} |
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 { | ||
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 call this |
||
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]) | ||
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. Why do we require functions with fixed sizes? Could we not return an object with an 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 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. 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. 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) | ||
} | ||
} |
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 | ||
|
||
} |
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]) = {} | ||
} |
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) | ||
} | ||
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,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") | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
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: