diff --git a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala index da83d0644dc2..bcda4b92fa96 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala @@ -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) + case tp: AndType => + hasRefinement(tp.tp1) && hasRefinement(tp.tp2) + case _ => + false + } + !tree.symbol.exists && tree.isTerm && hasRefinement(tree.qualifier.tpe) + case _ => + false + } } object TreeInfo { diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 45e37eb8bab3..1a6b04a18e43 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -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 @@ -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 diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 716959648a9e..4a9c50dad21a 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -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" diff --git a/compiler/src/dotty/tools/dotc/typer/Dynamic.scala b/compiler/src/dotty/tools/dotc/typer/Dynamic.scala index 4039c8b81a39..000cfd026c0f 100644 --- a/compiler/src/dotty/tools/dotc/typer/Dynamic.scala +++ b/compiler/src/dotty/tools/dotc/typer/Dynamic.scala @@ -7,10 +7,13 @@ 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 { @@ -18,7 +21,10 @@ object Dynamic { 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), ...) @@ -26,6 +32,10 @@ object Dynamic { * 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] + * + * 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") + } + } } diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 6dd3f45fc701..48636c27c430 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -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) } @@ -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 => diff --git a/library/src/scala/Selectable.scala b/library/src/scala/Selectable.scala new file mode 100644 index 000000000000..c5c714ca9c47 --- /dev/null +++ b/library/src/scala/Selectable.scala @@ -0,0 +1,8 @@ +package scala +import scala.reflect.ClassTag + +trait Selectable extends Any { + def selectDynamic(name: String): Any + def selectDynamicMethod(name: String, paramClasses: ClassTag[_]*): Any = + new UnsupportedOperationException("selectDynamicMethod") +} diff --git a/library/src/scala/reflect/Selectable.scala b/library/src/scala/reflect/Selectable.scala new file mode 100644 index 000000000000..0dbdbc293d1b --- /dev/null +++ b/library/src/scala/reflect/Selectable.scala @@ -0,0 +1,73 @@ +package scala.reflect + +class Selectable(val receiver: Any) extends AnyVal with scala.Selectable { + 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]) + } + } +} + +object Selectable { + implicit def reflectiveSelectable(receiver: Any): scala.Selectable = receiver match { + case receiver: scala.Selectable => receiver + case _ => new Selectable(receiver) + } +} diff --git a/tests/neg/structural.scala b/tests/neg/structural.scala new file mode 100644 index 000000000000..aab52b2cb74d --- /dev/null +++ b/tests/neg/structural.scala @@ -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 + +} diff --git a/tests/neg/zoo.scala b/tests/neg/zoo.scala index 19efcc1d7a44..1674548e842c 100644 --- a/tests/neg/zoo.scala +++ b/tests/neg/zoo.scala @@ -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 @@ -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 } diff --git a/tests/pos/i1866.scala b/tests/pos/i1866.scala new file mode 100644 index 000000000000..918d2e1827e6 --- /dev/null +++ b/tests/pos/i1866.scala @@ -0,0 +1,5 @@ +import scala.reflect.Selectable.reflectiveSelectable +object Test { + def f(g: { val update: Unit }) = g.update + def main(update: Array[String]) = {} +} diff --git a/tests/pos/zoo2.scala b/tests/pos/zoo2.scala new file mode 100644 index 000000000000..06210fe67feb --- /dev/null +++ b/tests/pos/zoo2.scala @@ -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) +} diff --git a/tests/run/structural.scala b/tests/run/structural.scala new file mode 100644 index 000000000000..0f18f4579184 --- /dev/null +++ b/tests/run/structural.scala @@ -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") + } +} + diff --git a/tests/run/structuralNoSuchMethod.check b/tests/run/structuralNoSuchMethod.check new file mode 100644 index 000000000000..20576fc9cfc6 --- /dev/null +++ b/tests/run/structuralNoSuchMethod.check @@ -0,0 +1 @@ +no such method diff --git a/tests/run/structuralNoSuchMethod.scala b/tests/run/structuralNoSuchMethod.scala new file mode 100644 index 000000000000..476d7ed8225c --- /dev/null +++ b/tests/run/structuralNoSuchMethod.scala @@ -0,0 +1,23 @@ +import scala.reflect.Selectable.reflectiveSelectable + +/** 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") + } + +}