Skip to content

Add scala.Dynamic support. #1291

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 3 commits into from
Jul 15, 2016
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
4 changes: 3 additions & 1 deletion src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -424,8 +424,10 @@ class Definitions {
def Product_productArity(implicit ctx: Context) = Product_productArityR.symbol
lazy val Product_productPrefixR = ProductClass.requiredMethodRef(nme.productPrefix)
def Product_productPrefix(implicit ctx: Context) = Product_productPrefixR.symbol
lazy val LanguageModuleRef = ctx.requiredModule("dotty.language")
lazy val LanguageModuleRef = ctx.requiredModule("dotty.language")
def LanguageModuleClass(implicit ctx: Context) = LanguageModuleRef.symbol.moduleClass.asClass
lazy val Scala2LanguageModuleRef = ctx.requiredModule("scala.language")
def Scala2LanguageModuleClass(implicit ctx: Context) = Scala2LanguageModuleRef.symbol.moduleClass.asClass
lazy val NonLocalReturnControlType: TypeRef = ctx.requiredClassRef("scala.runtime.NonLocalReturnControl")

lazy val ClassTagType = ctx.requiredClassRef("scala.reflect.ClassTag")
Expand Down
1 change: 1 addition & 0 deletions src/dotty/tools/dotc/core/StdNames.scala
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ object StdNames {
val delayedInit: N = "delayedInit"
val delayedInitArg: N = "delayedInit$body"
val drop: N = "drop"
val dynamics: N = "dynamics"
val dummyApply: N = "<dummy-apply>"
val elem: N = "elem"
val emptyValDef: N = "emptyValDef"
Expand Down
25 changes: 15 additions & 10 deletions src/dotty/tools/dotc/core/TypeOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -455,17 +455,19 @@ trait TypeOps { this: Context => // TODO: Make standalone object.
*/
def featureEnabled(owner: ClassSymbol, feature: TermName): Boolean = {
def toPrefix(sym: Symbol): String =
if (sym eq defn.LanguageModuleClass) "" else toPrefix(sym.owner) + sym.name + "."
if (!sym.exists || (sym eq defn.LanguageModuleClass) || (sym eq defn.Scala2LanguageModuleRef)) ""
else toPrefix(sym.owner) + sym.name + "."
def featureName = toPrefix(owner) + feature
def hasImport(implicit ctx: Context): Boolean = (
ctx.importInfo != null
&& ( (ctx.importInfo.site.widen.typeSymbol eq owner)
&& ctx.importInfo.originals.contains(feature)
||
{ var c = ctx.outer
while (c.importInfo eq ctx.importInfo) c = c.outer
hasImport(c)
}))
def hasImport(implicit ctx: Context): Boolean = {
if (ctx.importInfo == null || (ctx.importInfo.site.widen.typeSymbol ne owner)) false
else if (ctx.importInfo.excluded.contains(feature)) false
else if (ctx.importInfo.originals.contains(feature)) true
else {
var c = ctx.outer
while (c.importInfo eq ctx.importInfo) c = c.outer
hasImport(c)
}
}
def hasOption = ctx.base.settings.language.value exists (s => s == featureName || s == "_")
hasImport(ctx.withPhase(ctx.typerPhase)) || hasOption
}
Expand All @@ -477,6 +479,9 @@ trait TypeOps { this: Context => // TODO: Make standalone object.
def scala2Mode =
featureEnabled(defn.LanguageModuleClass, nme.Scala2)

def dynamicsEnabled =
featureEnabled(defn.Scala2LanguageModuleClass, nme.dynamics)

def testScala2Mode(msg: String, pos: Position) = {
if (scala2Mode) migrationWarning(msg, pos)
scala2Mode
Expand Down
3 changes: 3 additions & 0 deletions src/dotty/tools/dotc/core/Types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3021,6 +3021,9 @@ object Types {

object ErrorType extends ErrorType

/* Type used to track Select nodes that could not resolve a member and their qualifier is a scala.Dynamic. */
object TryDynamicCallType extends ErrorType

Copy link
Contributor

Choose a reason for hiding this comment

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

What happens if we try to map or fold over such a type? Did we think of that?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The idea is that it behaves like ErrorType on any operation until it is caught and handled. I considered making it a subtype of ErrorType but this seamed a bit dangerous and with this alternative there was only one place where I had to add it to a case to do the same as ErrorType.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually, now that I think about it, it should be object TryDynamicCallType extends ErrorType.

/** Wildcard type, possibly with bounds */
abstract case class WildcardType(optBounds: Type) extends CachedGroundType with TermType {
def derivedWildcardType(optBounds: Type)(implicit ctx: Context) =
Expand Down
30 changes: 29 additions & 1 deletion src/dotty/tools/dotc/reporting/Reporter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import config.Printers
import java.lang.System.currentTimeMillis
import core.Mode
import interfaces.Diagnostic.{ERROR, WARNING, INFO}
import dotty.tools.dotc.core.Symbols.Symbol

object Reporter {
class Error(msgFn: => String, pos: SourcePosition) extends Diagnostic(msgFn, pos, ERROR)
Expand Down Expand Up @@ -68,6 +69,29 @@ trait Reporting { this: Context =>
def featureWarning(msg: => String, pos: SourcePosition = NoSourcePosition): Unit =
reporter.report(new FeatureWarning(msg, pos))

def featureWarning(feature: String, featureDescription: String, isScala2Feature: Boolean,
featureUseSite: Symbol, required: Boolean, pos: SourcePosition): Unit = {
val req = if (required) "needs to" else "should"
val prefix = if (isScala2Feature) "scala." else "dotty."
val fqname = prefix + "language." + feature

val explain = {
if (reporter.isReportedFeatureUseSite(featureUseSite)) ""
else {
reporter.reportNewFeatureUseSite(featureUseSite)
s"""|
|This can be achieved by adding the import clause 'import $fqname'
|or by setting the compiler option -language:$feature.
|See the Scala docs for value $fqname for a discussion
|why the feature $req be explicitly enabled.""".stripMargin
}
}

val msg = s"$featureDescription $req be enabled\nby making the implicit value $fqname visible.$explain"
if (required) error(msg, pos)
else reporter.report(new FeatureWarning(msg, pos))
}

def warning(msg: => String, pos: SourcePosition = NoSourcePosition): Unit =
reporter.report(new Warning(msg, pos))

Expand Down Expand Up @@ -172,7 +196,7 @@ abstract class Reporter extends interfaces.ReporterResult {
/** Report a diagnostic */
def doReport(d: Diagnostic)(implicit ctx: Context): Unit

/** Whether very long lines can be truncated. This exists so important
/** Whether very long lines can be truncated. This exists so important
* debugging information (like printing the classpath) is not rendered
* invisible due to the max message length.
*/
Expand Down Expand Up @@ -206,6 +230,10 @@ abstract class Reporter extends interfaces.ReporterResult {
*/
def errorsReported = hasErrors

private[this] var reportedFeaturesUseSites = Set[Symbol]()
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be good to have a motivation why this is needed. In fact I am not yet sure we want to suppress feature warnings per use site.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Currently it does not suppress the warning. It is only there to make sure the explanation of how to avoid it is only printed once per site.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, OK, I had missed that.

def isReportedFeatureUseSite(featureTrait: Symbol): Boolean = reportedFeaturesUseSites.contains(featureTrait)
def reportNewFeatureUseSite(featureTrait: Symbol): Unit = reportedFeaturesUseSites += featureTrait

val unreportedWarnings = new mutable.HashMap[String, Int] {
override def default(key: String) = 0
}
Expand Down
10 changes: 9 additions & 1 deletion src/dotty/tools/dotc/typer/Applications.scala
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,12 @@ object Applications {

import Applications._

trait Applications extends Compatibility { self: Typer =>
trait Applications extends Compatibility { self: Typer with Dynamic =>

import Applications._
import tpd.{ cpy => _, _ }
import untpd.cpy
import Dynamic.isDynamicMethod

/** @tparam Arg the type of arguments, could be tpd.Tree, untpd.Tree, or Type
* @param methRef the reference to the method of the application
Expand Down Expand Up @@ -553,6 +554,13 @@ trait Applications extends Compatibility { self: Typer =>

fun1.tpe match {
case ErrorType => tree.withType(ErrorType)
case TryDynamicCallType =>
tree match {
case tree @ Apply(Select(qual, name), args) if !isDynamicMethod(name) =>
typedDynamicApply(qual, name, args, pt)(tree)
case _ =>
handleUnexpectedFunType(tree, fun1)
}
case _ => methPart(fun1).tpe match {
case funRef: TermRef =>
tryEither { implicit ctx =>
Expand Down
71 changes: 71 additions & 0 deletions src/dotty/tools/dotc/typer/Dynamic.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package dotty.tools
package dotc
package typer

import dotty.tools.dotc.ast.Trees.NamedArg
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.StdNames._
import dotty.tools.dotc.core.Types._
import dotty.tools.dotc.core.Mode
import dotty.tools.dotc.core.Decorators._

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:
* 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.
*/
trait Dynamic { self: Typer with Applications =>

/** Translate selection that does not typecheck according to the normal rules into a applyDynamic/applyDynamicNamed.
* foo.bar(baz0, baz1, ...) ~~> foo.applyDynamic(bar)(baz0, baz1, ...)
* foo.bar(x = bazX, y = bazY, baz, ...) ~~> foo.applyDynamicNamed("bar")(("x", bazX), ("y", bazY), ("", baz), ...)
*/
def typedDynamicApply(qual: untpd.Tree, name: Name, args: List[untpd.Tree], pt: Type)(original: untpd.Apply)(
implicit ctx: Context): Tree = {
def isNamedArg(arg: untpd.Tree): Boolean = arg match { case NamedArg(_, _) => true; case _ => false }
val dynName = if (args.exists(isNamedArg)) nme.applyDynamicNamed else nme.applyDynamic
if (dynName == nme.applyDynamicNamed && untpd.isWildcardStarArgList(args)) {
ctx.error("applyDynamicNamed does not support passing a vararg parameter", original.pos)
original.withType(ErrorType)
} else {
def namedArgTuple(name: String, arg: untpd.Tree) = untpd.Tuple(List(Literal(Constant(name)), arg))
def namedArgs = args.map {
case NamedArg(argName, arg) => namedArgTuple(argName.toString, arg)
case arg => namedArgTuple("", arg)
}
val args1 = if (dynName == nme.applyDynamic) args else namedArgs
typedApply(untpd.Apply(coreDynamic(qual, dynName, name), args1), pt)
}
}

/** Translate selection that does not typecheck according to the normal rules into a selectDynamic.
* foo.bar ~~> foo.selectDynamic(bar)
*
* Note: inner part of translation foo.bar(baz) = quux ~~> foo.selectDynamic(bar).update(baz, quux) is achieved
* through an existing transformation of in typedAssign [foo.bar(baz) = quux ~~> foo.bar.update(baz, quux)].
*/
def typedDynamicSelect(tree: untpd.Select, pt: Type)(implicit ctx: Context): Tree =
typedApply(coreDynamic(tree.qualifier, nme.selectDynamic, tree.name), pt)

/** Translate selection that does not typecheck according to the normal rules into a updateDynamic.
* foo.bar = baz ~~> foo.updateDynamic(bar)(baz)
*/
def typedDynamicAssign(qual: untpd.Tree, name: Name, rhs: untpd.Tree, pt: Type)(implicit ctx: Context): Tree =
typedApply(untpd.Apply(coreDynamic(qual, nme.updateDynamic, name), rhs), pt)

private def coreDynamic(qual: untpd.Tree, dynName: Name, name: Name)(implicit ctx: Context): untpd.Apply =
untpd.Apply(untpd.Select(qual, dynName), Literal(Constant(name.toString)))
}
2 changes: 2 additions & 0 deletions src/dotty/tools/dotc/typer/ProtoTypes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,8 @@ object ProtoTypes {
(if (theMap != null) theMap else new WildApproxMap).mapOver(tp)
}

@sharable object AssignProto extends UncachedGroundType with MatchAlways

private[ProtoTypes] class WildApproxMap(implicit ctx: Context) extends TypeMap {
def apply(tp: Type) = wildApprox(tp, this)
}
Expand Down
9 changes: 7 additions & 2 deletions src/dotty/tools/dotc/typer/TypeAssigner.scala
Original file line number Diff line number Diff line change
Expand Up @@ -196,11 +196,16 @@ trait TypeAssigner {
def selectionType(site: Type, name: Name, pos: Position)(implicit ctx: Context): Type = {
val mbr = site.member(name)
if (reallyExists(mbr)) site.select(name, mbr)
else {
else if (site.derivesFrom(defn.DynamicClass) && !Dynamic.isDynamicMethod(name)) {
TryDynamicCallType
} else {
if (!site.isErroneous) {
ctx.error(
if (name == nme.CONSTRUCTOR) d"$site does not have a constructor"
else d"$name is not a member of $site", pos)
else if (site.derivesFrom(defn.DynamicClass)) {
d"$name is not a member of $site\n" +
"possible cause: maybe a wrong Dynamic method signature?"
} else d"$name is not a member of $site", pos)
}
ErrorType
}
Expand Down
29 changes: 24 additions & 5 deletions src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,12 @@ object Typer {
assert(tree.pos.exists, s"position not set for $tree # ${tree.uniqueId}")
}

class Typer extends Namer with TypeAssigner with Applications with Implicits with Checking {
class Typer extends Namer with TypeAssigner with Applications with Implicits with Dynamic with Checking {

import Typer._
import tpd.{cpy => _, _}
import untpd.cpy
import Dynamic.isDynamicMethod

/** A temporary data item valid for a single typed ident:
* The set of all root import symbols that have been
Expand Down Expand Up @@ -316,7 +317,13 @@ class Typer extends Namer with TypeAssigner with Applications with Implicits wit
def asSelect(implicit ctx: Context): Tree = {
val qual1 = typedExpr(tree.qualifier, selectionProto(tree.name, pt, this))
if (tree.name.isTypeName) checkStable(qual1.tpe, qual1.pos)
typedSelect(tree, pt, qual1)
val select = typedSelect(tree, pt, qual1)
pt match {
case _: FunProto | AssignProto => select
case _ =>
if (select.tpe eq TryDynamicCallType) typedDynamicSelect(tree, pt)
else select
}
}

def asJavaSelectFromTypeTree(implicit ctx: Context): Tree = {
Expand Down Expand Up @@ -480,7 +487,7 @@ class Typer extends Namer with TypeAssigner with Applications with Implicits wit
val appliedUpdate = cpy.Apply(fn)(wrappedUpdate, (args map untpd.TypedSplice) :+ tree.rhs)
typed(appliedUpdate, pt)
case lhs =>
val lhsCore = typedUnadapted(lhs)
val lhsCore = typedUnadapted(lhs, AssignProto)
def lhs1 = typed(untpd.TypedSplice(lhsCore))
def canAssign(sym: Symbol) = // allow assignments from the primary constructor to class fields
sym.is(Mutable, butNot = Accessor) ||
Expand Down Expand Up @@ -508,6 +515,12 @@ class Typer extends Namer with TypeAssigner with Applications with Implicits wit
case _ =>
reassignmentToVal
}
case TryDynamicCallType =>
tree match {
case Assign(Select(qual, name), rhs) if !isDynamicMethod(name) =>
typedDynamicAssign(qual, name, rhs, pt)
case _ => reassignmentToVal
}
case tpe =>
reassignmentToVal
}
Expand Down Expand Up @@ -1079,7 +1092,13 @@ class Typer extends Namer with TypeAssigner with Applications with Implicits wit
.withType(dummy.nonMemberTermRef)
checkVariance(impl1)
if (!cls.is(AbstractOrTrait) && !ctx.isAfterTyper) checkRealizableBounds(cls.typeRef, cdef.pos)
assignType(cpy.TypeDef(cdef)(name, impl1, Nil), cls)
val cdef1 = assignType(cpy.TypeDef(cdef)(name, impl1, Nil), cls)
if (ctx.phase.isTyper && cdef1.tpe.derivesFrom(defn.DynamicClass) && !ctx.dynamicsEnabled) {
val isRequired = parents1.exists(_.tpe.isRef(defn.DynamicClass))
ctx.featureWarning(nme.dynamics.toString, "extension of type scala.Dynamic", isScala2Feature = true,
cls, isRequired, cdef.pos)
}
cdef1

// todo later: check that
// 1. If class is non-abstract, it is instantiatable:
Expand Down Expand Up @@ -1665,7 +1684,7 @@ class Typer extends Namer with TypeAssigner with Applications with Implicits wit
tree match {
case _: MemberDef | _: PackageDef | _: Import | _: WithoutTypeOrPos[_] => tree
case _ => tree.tpe.widen match {
case ErrorType =>
case _: ErrorType =>
tree
case ref: TermRef =>
pt match {
Expand Down
7 changes: 7 additions & 0 deletions tests/neg/dynamicApplyDynamicTest1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import scala.language.dynamics

class Foo extends scala.Dynamic

object DynamicTest {
new Foo().bazApply() // error
}
7 changes: 7 additions & 0 deletions tests/neg/dynamicApplyDynamicTest2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import scala.language.dynamics

class Foo extends scala.Dynamic

object DynamicTest {
new Foo().bazApply("abc", 1) // error
}
7 changes: 7 additions & 0 deletions tests/neg/dynamicApplyDynamicTest3.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import scala.language.dynamics

class Foo extends scala.Dynamic

object DynamicTest {
new Foo().bazApply _ // error // error
}
10 changes: 10 additions & 0 deletions tests/neg/dynamicApplyDynamicTest4.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import scala.language.dynamics

class Foo extends scala.Dynamic {
def selectDynamic(name: String): String = ???
def applyDynamicNamed(name: String)(args: Any*): String = ???
}

object DynamicTest {
new Foo().bazApply() // error
}
9 changes: 9 additions & 0 deletions tests/neg/dynamicApplyDynamicTest5.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import scala.language.dynamics

class Foo extends scala.Dynamic {
def applyDynamic(name: String)(args: String*): String = ???
}

object DynamicTest {
new Foo().bazApply(1, 2, 3) // error // error // error
}
9 changes: 9 additions & 0 deletions tests/neg/dynamicApplyDynamicTest6.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import scala.language.dynamics

class Foo extends scala.Dynamic {
def applyDynamic(name: String)(args: String*): String = ???
}

object DynamicTest {
def test: Int = new Foo().bazApply() // error
}
9 changes: 9 additions & 0 deletions tests/neg/dynamicApplyDynamicTest7.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import scala.language.dynamics

class Foo extends scala.Dynamic {
def applyDynamic(name: Int)(args: String*): String = ???
}

object DynamicTest {
def test: String = new Foo().bazApply() // error
}
Loading