Skip to content

Handle structural types with curried methods #5391

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 1 commit into from
Nov 8, 2018
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
31 changes: 14 additions & 17 deletions compiler/src/dotty/tools/dotc/ast/TreeInfo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -737,22 +737,11 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] =>
case _ => This(ctx.owner.enclosingClass.asClass)
}

/** Is this an application on a structural selection?
*
* @see isStructuralTermSelect
*/
def isStructuralTermApply(tree: Tree)(implicit ctx: Context): Boolean = tree match {
case Apply(fun, _) =>
isStructuralTermSelect(fun)
case _ =>
false
}

/** Is this a selection of a member of a structural type that is not a member
* of an underlying class or trait?
/** Is this a (potentially applied) 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): Boolean = tree match {
case tree: Select =>
def isStructuralTermSelectOrApply(tree: Tree)(implicit ctx: Context): Boolean = {
def isStructuralTermSelect(tree: Select) = {
def hasRefinement(qualtpe: Type): Boolean = qualtpe.dealias match {
case RefinedType(parent, rname, rinfo) =>
rname == tree.name || hasRefinement(parent)
Expand All @@ -766,8 +755,16 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] =>
false
}
!tree.symbol.exists && tree.isTerm && hasRefinement(tree.qualifier.tpe)
case _ =>
false
}
def loop(tree: Tree): Boolean = tree match {
case Apply(fun, _) =>
loop(fun)
case tree: Select =>
isStructuralTermSelect(tree)
case _ =>
false
}
loop(tree)
}

/** Is this call a call to a method that is marked as Inline */
Expand Down
68 changes: 40 additions & 28 deletions compiler/src/dotty/tools/dotc/typer/Dynamic.scala
Original file line number Diff line number Diff line change
Expand Up @@ -117,61 +117,73 @@ trait Dynamic { self: Typer with Applications =>
* Given `x.a`, where `x` is of (widened) type `T` (a value type or a nullary method type),
* and `x.a` is of type `U`, map `x.a` to the equivalent of:
*
* (x: Selectable).selectDynamic("a").asInstanceOf[U]
* ```scala
* (x: Selectable).selectDynamic("a").asInstanceOf[U]
* ```
*
* Given `x.a(arg1, ..., argn)`, where `x.a` is of (widened) type (T1, ..., Tn)R,
* map `x.a(arg1, ..., argn)` to the equivalent of:
* Given `x.a(a11, ..., a1n)...(aN1, ..., aNn)`, where `x.a` is of (widened) type
* `(T11, ..., T1n)...(TN1, ..., TNn) => R`, it is desugared to:
*
* (x:selectable).applyDynamic("a", CT1, ..., CTn)(arg1, ..., argn).asInstanceOf[R]
* ```scala
* (x:selectable).applyDynamic("a", CT11, ..., CT1n, ..., CTN1, ... CTNn)
* (a11, ..., a1n, ..., aN1, ..., aNn)
* .asInstanceOf[R]
* ```
*
* where CT1, ..., CTn are the class tags representing the erasure of T1, ..., Tn.
* where CT11, ..., CTNn are the class tags representing the erasure of T11, ..., TNn.
*
* It's an error if U is neither a value nor a method type, or a dependent method
* type.
*/
def handleStructural(tree: Tree)(implicit ctx: Context): Tree = {
val (fun @ Select(qual, name), targs, vargss) = decomposeCall(tree)

def structuralCall(qual: Tree, name: Name, selectorName: TermName, ctags: List[Tree], args: Option[List[Tree]]) = {
def structuralCall(selectorName: TermName, ctags: List[Tree]) = {
val selectable = adapt(qual, defn.SelectableType)

// ($qual: Selectable).$selectorName("$name", ..$ctags)
val base =
untpd.Apply(
untpd.TypedSplice(selectable.select(selectorName)).withPos(tree.pos),
untpd.TypedSplice(selectable.select(selectorName)).withPos(fun.pos),
(Literal(Constant(name.toString)) :: ctags).map(untpd.TypedSplice(_)))

val scall = args match {
case None => base
case Some(args) => untpd.Apply(base, args)
}
val scall =
if (vargss.isEmpty) base
else untpd.Apply(base, vargss.flatten)

typed(scall)
}

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

val tpe = tree.tpe.widen
tree match {
case Apply(fun @ Select(qual, name), args) =>
val funTpe = fun.tpe.widen.asInstanceOf[MethodType]
if (funTpe.isParamDependent)
fun.tpe.widen match {
case tpe: ValueType =>
structuralCall(nme.selectDynamic, Nil).asInstance(tpe)

case tpe: MethodType =>
def isDependentMethod(tpe: Type): Boolean = tpe match {
case tpe: MethodType =>
tpe.isParamDependent ||
tpe.isResultDependent ||
isDependentMethod(tpe.resultType)
case _ =>
false
}

if (isDependentMethod(tpe))
fail(name, i"has a method type with inter-parameter dependencies")
else {
val ctags = funTpe.paramInfos.map(pt =>
implicitArgTree(defn.ClassTagType.appliedTo(pt :: Nil), tree.pos.endPos))
structuralCall(qual, name, nme.applyDynamic, ctags, Some(args)).asInstance(tpe.resultType)
val ctags = tpe.paramInfoss.flatten.map(pt =>
implicitArgTree(defn.ClassTagType.appliedTo(pt.widenDealias :: Nil), fun.pos.endPos))
structuralCall(nme.applyDynamic, ctags).asInstance(tpe.finalResultType)
}
case Select(qual, name) if tpe.isValueType =>
structuralCall(qual, name, nme.selectDynamic, Nil, None).asInstance(tpe)
case Select(_, _) if !tpe.isParameterless =>
// We return the tree unchanged; The structural call will be handled when we take care of the
// enclosing application.
tree
case Select(_, name) if tpe.isInstanceOf[PolyType] =>

// (@allanrenucci) I think everything below is dead code
case _: PolyType =>
fail(name, "is polymorphic")
case Select(_, name) =>
fail(name, i"has an unsupported type: ${tree.tpe.widen}")
case tpe =>
fail(name, i"has an unsupported type: $tpe")
}
}
}
5 changes: 2 additions & 3 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2640,9 +2640,8 @@ class Typer extends Namer
convertNewGenericArray(readapt(tree.appliedToTypeTrees(typeArgs)))
}
case wtp =>
if (isStructuralTermApply(tree))
readaptSimplified(handleStructural(tree))
else if (isStructuralTermSelect(tree) && tree.tpe.widen.isValueType)
val isStructuralCall = wtp.isValueType && isStructuralTermSelectOrApply(tree)
if (isStructuralCall)
readaptSimplified(handleStructural(tree))
else pt match {
case pt: FunProto =>
Expand Down
10 changes: 6 additions & 4 deletions docs/docs/reference/changed/structural-types-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,19 @@ consider three distinct cases:
(v: Selectable).selectDynamic("a").asInstanceOf[U]
```

- If `U` is a method type `(T1, ..., Tn) => R` with at most 7
parameters and it is not a dependent method type, we map `v.a` to
- If `U` is a method type `(T11, ..., T1n)...(TN1, ..., TNn) => R` and it is not
a dependent method type, we map `v.a(a11, ..., a1n)...(aN1, aNn)` to
the equivalent of:
```scala
v.a(arg1, ..., argn)
--->
(v: Selectable).applyDynamic("a", CT1, ..., CTn)(arg1, ..., argn).asInstanceOf[R]
(v: Selectable).applyDynamic("a", CT11, ..., CTn, ..., CTN1, ... CTNn)
(a11, ..., a1n, ..., aN1, ..., aNn)
.asInstanceOf[R]
```

- If `U` is neither a value nor a method type, or a dependent method
type, or has more than 7 parameters, an error is emitted.
type, an error is emitted.

We make sure that `r` conforms to type `Selectable`, potentially by
introducing an implicit conversion, and then call either
Expand Down
2 changes: 1 addition & 1 deletion library/src/scala/reflect/Selectable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class Selectable(val receiver: Any) extends AnyVal with scala.Selectable {
val paramClasses = paramTypes.map(_.runtimeClass)
val mth = rcls.getMethod(name, paramClasses: _*)
ensureAccessible(mth)
mth.invoke(receiver, args.map(_.asInstanceOf[AnyRef]): _*)
mth.invoke(receiver, args.asInstanceOf[Seq[AnyRef]]: _*)
}
}

Expand Down
16 changes: 16 additions & 0 deletions tests/neg/structural.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,20 @@ object Test3 {

type G = { def foo(x: Int, y: Int): Unit }
def j(x: G) = x.foo(???) // error: missing argument

class H { type S = String; type I }
class I extends H { type I = Int }
type Dep = {
def fun1(x: H, y: x.S): Int
def fun2(x: H, y: x.I): Int
def fun3(y: H): y.S
def fun4(y: H): y.I
}
def k(x: Dep) = {
val y = new I
x.fun1(y, "Hello")
x.fun2(y, 1) // error
x.fun3(y)
x.fun4(y) // error
}
}
106 changes: 106 additions & 0 deletions tests/run/structural.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import scala.reflect.Selectable.reflectiveSelectable

object Test {
class C { type S = String; type I }
class D extends C { type I = Int }

type Foo = {
def sel0: Int
def sel1: Int => Int
def fun0(x: Int): Int

def fun1(x: Int)(y: Int): Int
def fun2(x: Int): Int => Int
def fun3(a1: Int, a2: Int, a3: Int)
(a4: Int, a5: Int, a6: Int)
(a7: Int, a8: Int, a9: Int): Int

def fun4(implicit x: Int): Int
def fun5(x: Int)(implicit y: Int): Int

def fun6(x: C, y: x.S): Int
def fun7(x: C, y: x.I): Int
def fun8(y: C): y.S
def fun9(y: C): y.I
}

class FooI {
def sel0: Int = 1
def sel1: Int => Int = x => x
def fun0(x: Int): Int = x

def fun1(x: Int)(y: Int): Int = x + y
def fun2(x: Int): Int => Int = y => x * y
def fun3(a1: Int, a2: Int, a3: Int)
(a4: Int, a5: Int, a6: Int)
(a7: Int, a8: Int, a9: Int): Int = -1

def fun4(implicit x: Int): Int = x
def fun5(x: Int)(implicit y: Int): Int = x + y

def fun6(x: C, y: x.S): Int = 1
def fun7(x: C, y: x.I): Int = 2
def fun8(y: C): y.S = "Hello"
def fun9(y: C): y.I = 1.asInstanceOf[y.I]
}

def basic(x: Foo): Unit ={
assert(x.sel0 == 1)
assert(x.sel1(2) == 2)
assert(x.fun0(3) == 3)

val f = x.sel1
assert(f(3) == 3)
}

def currying(x: Foo): Unit = {
assert(x.fun1(1)(2) == 3)
assert(x.fun2(1)(2) == 2)
assert(x.fun3(1, 2, 3)(4, 5, 6)(7, 8, 9) == -1)
}

def etaExpansion(x: Foo): Unit = {
val f0 = x.fun0(_)
assert(f0(2) == 2)

val f1 = x.fun0 _
assert(f1(2) == 2)

val f2 = x.fun1(1)(_)
assert(f2(2) == 3)

val f3 = x.fun1(1) _
assert(f3(2) == 3)

val f4 = x.fun1(1)
assert(f4(2) == 3)
}

def implicits(x: Foo) = {
implicit val y = 2
assert(x.fun4 == 2)
assert(x.fun5(1) == 3)
}

// Limited support for dependant methods
def dependant(x: Foo) = {
val y = new D

assert(x.fun6(y, "Hello") == 1)
// assert(x.fun7(y, 1) == 2) // error: No ClassTag available for x.I

val s = x.fun8(y)
assert((s: String) == "Hello")

// val i = x.fun9(y) // error: rejected (blows up in pickler if not rejected)
// assert((i: String) == "Hello") // error: Type mismatch: found: y.S(i); required: String
}

def main(args: Array[String]): Unit = {
basic(new FooI)
currying(new FooI)
etaExpansion(new FooI)
implicits(new FooI)
dependant(new FooI)
}
}