summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/compiler/scala/tools/nsc/typechecker/Macros.scala64
-rw-r--r--test/files/run/t5923a/Macros_1.scala42
-rw-r--r--test/files/run/t5923c.check1
-rw-r--r--test/files/run/t5923c/Macros_1.scala39
-rw-r--r--test/files/run/t5923c/Test_2.scala12
-rw-r--r--test/files/run/t5923d.check0
-rw-r--r--test/files/run/t5923d/Macros_1.scala9
-rw-r--r--test/files/run/t5923d/Test_2.scala7
8 files changed, 160 insertions, 14 deletions
diff --git a/src/compiler/scala/tools/nsc/typechecker/Macros.scala b/src/compiler/scala/tools/nsc/typechecker/Macros.scala
index 6b9537e27d..b3675d6a82 100644
--- a/src/compiler/scala/tools/nsc/typechecker/Macros.scala
+++ b/src/compiler/scala/tools/nsc/typechecker/Macros.scala
@@ -589,18 +589,23 @@ trait Macros extends FastTrack with MacroRuntimes with Traces with Helpers {
/** Expands a term macro used in apply role as `M(2)(3)` in `val x = M(2)(3)`.
* @see MacroExpander
*/
- def macroExpandApply(typer: Typer, expandee: Tree, mode: Mode, pt: Type) = {
+ def macroExpandApply(typer: Typer, expandee: Tree, mode: Mode, pt: Type): Tree = {
object expander extends TermMacroExpander(APPLY_ROLE, typer, expandee, mode, pt) {
override def onSuccess(expanded: Tree) = {
// prematurely annotate the tree with a macro expansion attachment
// so that adapt called indirectly by typer.typed knows that it needs to apply the existential fixup
linkExpandeeAndExpanded(expandee, expanded)
- var expectedTpe = expandee.tpe
- if (isNullaryInvocation(expandee)) expectedTpe = expectedTpe.finalResultType
+ // approximation is necessary for whitebox macros to guide type inference
+ // read more in the comments for onDelayed below
+ def approximate(tp: Type) = {
+ val undetparams = tp collect { case tp if tp.typeSymbol.isTypeParameter => tp.typeSymbol }
+ deriveTypeWithWildcards(undetparams)(tp)
+ }
+ val macroPtApprox = approximate(if (isNullaryInvocation(expandee)) expandee.tpe.finalResultType else expandee.tpe)
// `macroExpandApply` is called from `adapt`, where implicit conversions are disabled
// therefore we need to re-enable the conversions back temporarily
- if (macroDebugVerbose) println(s"typecheck #1 (against expectedTpe = $expectedTpe): $expanded")
- val expanded1 = typer.context.withImplicitsEnabled(typer.typed(expanded, mode, expectedTpe))
+ if (macroDebugVerbose) println(s"typecheck #1 (against macroPtApprox = $macroPtApprox): $expanded")
+ val expanded1 = typer.context.withImplicitsEnabled(typer.typed(expanded, mode, macroPtApprox))
if (expanded1.isErrorTyped) {
if (macroDebugVerbose) println(s"typecheck #1 has failed: ${typer.context.reportBuffer.errors}")
expanded1
@@ -612,6 +617,8 @@ trait Macros extends FastTrack with MacroRuntimes with Traces with Helpers {
}
}
override def onDelayed(delayed: Tree) = {
+ // =========== THE SITUATION ===========
+ //
// If we've been delayed (i.e. bailed out of the expansion because of undetermined type params present in the expandee),
// then there are two possible situations we're in:
// 1) We're in POLYmode, when the typer tests the waters wrt type inference
@@ -627,12 +634,43 @@ trait Macros extends FastTrack with MacroRuntimes with Traces with Helpers {
// the undetermined type params. Therefore we need to do something ourselves or otherwise this
// expandee will forever remaing not expanded (see SI-5692). A traditional way out of this conundrum
// is to call `instantiate` and let the inferencer try to find the way out. It works for simple cases,
- // but sometimes, if the inferencer lacks information, it will be forced to approximate. This prevents
- // an important class of macros, fundep materializers, from working, which I perceive is a problem we need to solve.
- // For details see SI-7470.
+ // but sometimes, if the inferencer lacks information, it will be forced to approximate.
+ //
+ // =========== THE PROBLEM ===========
+ //
+ // Consider the following example (thanks, Miles!):
+ //
+ // Iso represents an isomorphism between two datatypes:
+ // 1) An arbitrary one (e.g. a random case class)
+ // 2) A uniform representation for all datatypes (e.g. an HList)
+ //
+ // trait Iso[T, U] {
+ // def to(t : T) : U
+ // def from(u : U) : T
+ // }
+ // implicit def materializeIso[T, U]: Iso[T, U] = macro ???
+ //
+ // case class Foo(i: Int, s: String, b: Boolean)
+ // def foo[C, L](c: C)(implicit iso: Iso[C, L]): L = iso.to(c)
+ // foo(Foo(23, "foo", true))
+ //
+ // In the snippet above, even though we know that there's a fundep going from T to U
+ // (in a sense that a datatype's uniform representation is unambiguously determined by the datatype,
+ // e.g. for Foo it will be Int :: String :: Boolean :: HNil), there's no way to convey this information
+ // to the typechecker. Therefore the typechecker will infer Nothing for L, which is hardly what we want.
+ //
+ // =========== THE SOLUTION ===========
+ //
+ // To give materializers a chance to say their word before vanilla inference kicks in,
+ // we infer as much as possible (e.g. in the example above even though L is hopeless, C still can be inferred to Foo)
+ // and then trigger macro expansion with the undetermined type parameters still there.
+ // Thanks to that the materializer can take a look at what's going on and react accordingly.
val shouldInstantiate = typer.context.undetparams.nonEmpty && !mode.inPolyMode
- if (shouldInstantiate) typer.instantiatePossiblyExpectingUnit(delayed, mode, pt)
- else delayed
+ if (shouldInstantiate) {
+ forced += delayed
+ typer.infer.inferExprInstance(delayed, typer.context.extractUndetparams(), pt, keepNothings = false)
+ macroExpandApply(typer, delayed, mode, pt)
+ } else delayed
}
}
expander(expandee)
@@ -750,10 +788,12 @@ trait Macros extends FastTrack with MacroRuntimes with Traces with Helpers {
* 2) undetparams (sym.isTypeParameter && !sym.isSkolem)
*/
var hasPendingMacroExpansions = false
+ private val forced = perRunCaches.newWeakSet[Tree]
private val delayed = perRunCaches.newWeakMap[Tree, scala.collection.mutable.Set[Int]]()
private def isDelayed(expandee: Tree) = delayed contains expandee
private def calculateUndetparams(expandee: Tree): scala.collection.mutable.Set[Int] =
- delayed.get(expandee).getOrElse {
+ if (forced(expandee)) scala.collection.mutable.Set[Int]()
+ else delayed.getOrElse(expandee, {
val calculated = scala.collection.mutable.Set[Symbol]()
expandee foreach (sub => {
def traverse(sym: Symbol) = if (sym != null && (undetparams contains sym.id)) calculated += sym
@@ -762,7 +802,7 @@ trait Macros extends FastTrack with MacroRuntimes with Traces with Helpers {
})
macroLogVerbose("calculateUndetparams: %s".format(calculated))
calculated map (_.id)
- }
+ })
private val undetparams = perRunCaches.newSet[Int]()
def notifyUndetparamsAdded(newUndets: List[Symbol]): Unit = {
undetparams ++= newUndets map (_.id)
diff --git a/test/files/run/t5923a/Macros_1.scala b/test/files/run/t5923a/Macros_1.scala
index 6d21362c4d..97076eb102 100644
--- a/test/files/run/t5923a/Macros_1.scala
+++ b/test/files/run/t5923a/Macros_1.scala
@@ -7,8 +7,46 @@ object C {
}
object Macros {
- def impl[T: c.WeakTypeTag](c: Context) = {
+ def impl[T](c: Context)(ttag: c.WeakTypeTag[T]) = {
import c.universe._
- reify(C[T](c.literal(weakTypeOf[T].toString).splice))
+ val ttag0 = ttag;
+ {
+ // When we're expanding implicitly[C[Nothing]], the type inferencer will see
+ // that foo[T] returns C[T] and that we request an implicit of type C[Nothing].
+ //
+ // Then the type inferencer will try to match C[T] against C[Nothing] and infer everything it can infer
+ // from that match, but not more (e.g. if we were returning Iso[T, U] and the type we were looking at was Iso[Foo, L],
+ // we wouldn't want U to be auto-inferred to Nothing, as it usually happens with normal methods,
+ // but would rather want it to remain unknown, so that our macro could take a stab at inferring it:
+ // see the comments in this commit for more information).
+ //
+ // Equipped with common sense, in our case of C[T] and C[Nothing] we would expect T to be inferred as Nothing, and then we
+ // would expect T in the corresponding macro invocation to be Nothing. Unfortunately it is not that simple.
+ //
+ // Internally the type inferencer uses Nothing as a dummy value, which stands for "don't know how to
+ // infer this type parameter". In the Iso example, matching Iso[T, U] against Iso[Foo, L] would result in
+ // T being inferred as Foo and U being inferred as Nothing (!!). Then the type inferencer will think:
+ // "Aha! U ended up being Nothing. This means that I failed to infer it,
+ // therefore the result of my work is: T -> Foo, U -> still unknown".
+ //
+ // That's all very good and works very well until Nothing is a genuine result of type inference,
+ // as in our original example of inferring T in C[T] from C[Nothing]. In that case, the inferencer becomes confused
+ // and here in the macro implementation we get weakTypeOf[T] equal to some dummy type carrying a type parameter
+ // instead of Nothing.
+ //
+ // This eccentric behavior of the type inferencer is a long-standing problem in scalac,
+ // so the best one can do for now until it's fixed is to work around, manually converting
+ // suspicious T's into Nothings. Of course, this means that we would have to approximate,
+ // because there's no way to know whether having T here stands for a failed attempt to infer Nothing
+ // or for a failed attempt to infer anything, but at least we're in full control of making the best
+ // of this sad situation.
+ implicit def ttag: WeakTypeTag[T] = {
+ val tpe = ttag0.tpe
+ val sym = tpe.typeSymbol.asType
+ if (sym.isParameter && !sym.isSkolem) TypeTag.Nothing.asInstanceOf[TypeTag[T]]
+ else ttag0
+ }
+ reify(C[T](c.literal(weakTypeOf[T].toString).splice))
+ }
}
} \ No newline at end of file
diff --git a/test/files/run/t5923c.check b/test/files/run/t5923c.check
new file mode 100644
index 0000000000..bed7429108
--- /dev/null
+++ b/test/files/run/t5923c.check
@@ -0,0 +1 @@
+(23,foo,true)
diff --git a/test/files/run/t5923c/Macros_1.scala b/test/files/run/t5923c/Macros_1.scala
new file mode 100644
index 0000000000..0b7a3399e2
--- /dev/null
+++ b/test/files/run/t5923c/Macros_1.scala
@@ -0,0 +1,39 @@
+import language.experimental.macros
+import scala.reflect.macros.Context
+
+trait Iso[T, U] {
+ def to(t : T) : U
+ // def from(u : U) : T
+}
+
+object Iso {
+ implicit def materializeIso[T, U]: Iso[T, U] = macro impl[T, U]
+ def impl[T: c.WeakTypeTag, U: c.WeakTypeTag](c: Context): c.Expr[Iso[T, U]] = {
+ import c.universe._
+ import definitions._
+ import Flag._
+
+ val sym = c.weakTypeOf[T].typeSymbol
+ if (!sym.isClass || !sym.asClass.isCaseClass) c.abort(c.enclosingPosition, s"$sym is not a case class")
+ val fields = sym.typeSignature.declarations.toList.collect{ case x: TermSymbol if x.isVal && x.isCaseAccessor => x }
+
+ def mkTpt() = {
+ val core = Ident(TupleClass(fields.length) orElse UnitClass)
+ if (fields.length == 0) core
+ else AppliedTypeTree(core, fields map (f => TypeTree(f.typeSignature)))
+ }
+
+ def mkFrom() = {
+ if (fields.length == 0) Literal(Constant(Unit))
+ else Apply(Ident(newTermName("Tuple" + fields.length)), fields map (f => Select(Ident(newTermName("f")), newTermName(f.name.toString.trim))))
+ }
+
+ val evidenceClass = ClassDef(Modifiers(FINAL), newTypeName("$anon"), List(), Template(
+ List(AppliedTypeTree(Ident(newTypeName("Iso")), List(Ident(sym), mkTpt()))),
+ emptyValDef,
+ List(
+ DefDef(Modifiers(), nme.CONSTRUCTOR, List(), List(List()), TypeTree(), Block(List(Apply(Select(Super(This(tpnme.EMPTY), tpnme.EMPTY), nme.CONSTRUCTOR), List())), Literal(Constant(())))),
+ DefDef(Modifiers(), newTermName("to"), List(), List(List(ValDef(Modifiers(PARAM), newTermName("f"), Ident(sym), EmptyTree))), TypeTree(), mkFrom()))))
+ c.Expr[Iso[T, U]](Block(List(evidenceClass), Apply(Select(New(Ident(newTypeName("$anon"))), nme.CONSTRUCTOR), List())))
+ }
+}
diff --git a/test/files/run/t5923c/Test_2.scala b/test/files/run/t5923c/Test_2.scala
new file mode 100644
index 0000000000..a00f4ed7db
--- /dev/null
+++ b/test/files/run/t5923c/Test_2.scala
@@ -0,0 +1,12 @@
+// see the comments for macroExpandApply.onDelayed for an explanation of what's tested here
+object Test extends App {
+ case class Foo(i: Int, s: String, b: Boolean)
+ def foo[C, L](c: C)(implicit iso: Iso[C, L]): L = iso.to(c)
+
+ {
+ val equiv = foo(Foo(23, "foo", true))
+ def typed[T](t: => T) {}
+ typed[(Int, String, Boolean)](equiv)
+ println(equiv)
+ }
+} \ No newline at end of file
diff --git a/test/files/run/t5923d.check b/test/files/run/t5923d.check
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/test/files/run/t5923d.check
diff --git a/test/files/run/t5923d/Macros_1.scala b/test/files/run/t5923d/Macros_1.scala
new file mode 100644
index 0000000000..f32d1af704
--- /dev/null
+++ b/test/files/run/t5923d/Macros_1.scala
@@ -0,0 +1,9 @@
+import scala.language.experimental.macros
+import scala.reflect.macros.Context
+
+trait MappedRow
+trait RowMapper[T <: MappedRow]
+object RowMapper {
+ implicit def mapper[T <: MappedRow]: RowMapper[T] = macro impl[T]
+ def impl[T <: MappedRow : c.WeakTypeTag](c: Context) = c.universe.reify(new RowMapper[T]{})
+} \ No newline at end of file
diff --git a/test/files/run/t5923d/Test_2.scala b/test/files/run/t5923d/Test_2.scala
new file mode 100644
index 0000000000..6be10227c2
--- /dev/null
+++ b/test/files/run/t5923d/Test_2.scala
@@ -0,0 +1,7 @@
+class RowA extends MappedRow
+class RowB extends MappedRow
+
+object Test extends App {
+ implicitly[RowMapper[RowA]]
+ implicitly[RowMapper[RowB]]
+} \ No newline at end of file