Skip to content

Commit

Permalink
newsubtype
Browse files Browse the repository at this point in the history
  • Loading branch information
carymrobbins committed Mar 17, 2018
1 parent 59fbe07 commit 2758733
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,23 @@ private[macros] class NewTypeMacros(val c: blackbox.Context) {

import c.universe._

def newtypeAnnotation(annottees: Tree*): Tree = {
def newtypeAnnotation(annottees: Tree*): Tree =
runAnnotation(subtype = false, annottees)

def newsubtypeAnnotation(annottees: Tree*): Tree =
runAnnotation(subtype = true, annottees)

def runAnnotation(subtype: Boolean, annottees: Seq[Tree]): Tree = {
val (name, result) = annottees match {
case List(clsDef: ClassDef) => (clsDef.name, runClass(clsDef))
case List(clsDef: ClassDef, modDef: ModuleDef) => (clsDef.name, runClassWithObj(clsDef, modDef))
case _ => fail("Unsupported newtype definition")
case List(clsDef: ClassDef) =>
(clsDef.name, runClass(clsDef, subtype))
case List(clsDef: ClassDef, modDef: ModuleDef) =>
(clsDef.name, runClassWithObj(clsDef, modDef, subtype))
case _ =>
fail(s"Unsupported @$macroName definition")
}
if (debug) println(s"Expanded @newtype $name:\n" ++ show(result))
if (debugRaw) println(s"Expanded @newtype $name (raw):\n" + showRaw(result))
if (debug) println(s"Expanded @$macroName $name:\n" ++ show(result))
if (debugRaw) println(s"Expanded @$macroName $name (raw):\n" + showRaw(result))
result
}

Expand Down Expand Up @@ -57,11 +66,11 @@ private[macros] class NewTypeMacros(val c: blackbox.Context) {

def fail(msg: String) = c.abort(c.enclosingPosition, msg)

def runClass(clsDef: ClassDef) = {
runClassWithObj(clsDef, q"object ${clsDef.name.toTermName}".asInstanceOf[ModuleDef])
def runClass(clsDef: ClassDef, subtype: Boolean) = {
runClassWithObj(clsDef, q"object ${clsDef.name.toTermName}".asInstanceOf[ModuleDef], subtype)
}

def runClassWithObj(clsDef: ClassDef, modDef: ModuleDef) = {
def runClassWithObj(clsDef: ClassDef, modDef: ModuleDef, subtype: Boolean) = {
val valDef = extractConstructorValDef(getConstructor(clsDef.impl.body))
// Converts [F[_], A] to [F, A]; needed for applying the defined type params.
val tparamNames: List[TypeName] = clsDef.tparams.map(_.name)
Expand All @@ -75,19 +84,37 @@ private[macros] class NewTypeMacros(val c: blackbox.Context) {
// Ensure we're not trying to inherit from anything.
validateParents(clsDef.impl.parents)
// Build the type and object definitions.
generateNewType(clsDef, modDef, valDef, tparamsNoVar, tparamNames, tparamsWild)
generateNewType(clsDef, modDef, valDef, tparamsNoVar, tparamNames, tparamsWild, subtype)
}

def mkBaseTypeDef(clsDef: ClassDef, reprType: Tree, subtype: Boolean) = {
val refinementName = TypeName(clsDef.name.decodedName.toString + "$newtype")
(clsDef.tparams, subtype) match {
case (_, false) => q"type Base = { type $refinementName } "
case (Nil, true) => q"type Base = $reprType"
case (tparams, true) => q"type Base[..$tparams] = $reprType"
}
}

def mkTypeTypeDef(clsDef: ClassDef, tparamsNames: List[TypeName], subtype: Boolean) =
(clsDef.tparams, subtype) match {
case (Nil, false) => q"type Type <: Base with Tag"
case (tparams, false) => q"type Type[..$tparams] <: Base with Tag[..$tparamsNames]"
case (Nil, true) => q"type Type = Base with Tag"
case (tparams, true) => q"type Type[..$tparams] = Base[..$tparamsNames] with Tag[..$tparamsNames]"
}

def generateNewType(
clsDef: ClassDef, modDef: ModuleDef, valDef: ValDef,
tparamsNoVar: List[TypeDef], tparamNames: List[TypeName], tparamsWild: List[TypeDef]
tparamsNoVar: List[TypeDef], tparamNames: List[TypeName], tparamsWild: List[TypeDef],
subtype: Boolean
): Tree = {
val q"object $objName extends { ..$objEarlyDefs } with ..$objParents { $objSelf => ..$objDefs }" = modDef
val typeName = clsDef.name
val clsName = clsDef.name.decodedName
val reprType = valDef.tpt
val typesTraitName = TypeName(clsName.toString + '$' + "Types")
val tparams = clsDef.tparams
val baseRefinementName = TypeName(clsName + "$newtype")
val classTagName = TermName(clsName + "$classTag")
val companionExtraDefs =
generateClassTag(classTagName, tparamsNoVar, tparamNames) ::
Expand All @@ -103,32 +130,37 @@ private[macros] class NewTypeMacros(val c: blackbox.Context) {
..$companionExtraDefs
}
"""

// Note that we use an abstract type alias
// `type Type <: Base with Tag` and not `type Type = ...` to prevent
// scalac automatically expanding the type alias.
// Also, Scala 2.10 doesn't support objects having abstract type members, so we have to
// use some indirection by defining the abstract type in a trait then having
// the companion object extend the trait.
// See https://github.com/scala/bug/issues/10750

val baseTypeDef = mkBaseTypeDef(clsDef, reprType, subtype)
val typeTypeDef = mkTypeTypeDef(clsDef, tparamNames, subtype)

if (tparams.isEmpty) {
q"""
type $typeName = $objName.Type
trait $typesTraitName {
type Repr = ${valDef.tpt}
type Base = { type $baseRefinementName }
type Repr = $reprType
$baseTypeDef
trait Tag
type Type <: Base with Tag
${mkTypeTypeDef(clsDef, tparamNames, subtype)}
}
$newtypeObjDef
"""
} else {
q"""
type $typeName[..$tparams] = ${typeName.toTermName}.Type[..$tparamNames]
trait $typesTraitName {
type Repr[..$tparams] = ${valDef.tpt}
type Base = { type $baseRefinementName }
type Repr[..$tparams] = $reprType
$baseTypeDef
trait Tag[..$tparams]
type Type[..$tparams] <: Base with Tag[..$tparamNames]
$typeTypeDef
}
$newtypeObjDef
"""
Expand All @@ -143,9 +175,9 @@ private[macros] class NewTypeMacros(val c: blackbox.Context) {
q"def apply(${valDef.name}: ${valDef.tpt}): Type = ${valDef.name}.asInstanceOf[Type]"
} else {
q"""
def apply[..$tparamsNoVar](${valDef.name}: ${valDef.tpt}): Type[..$tparamNames] =
${valDef.name}.asInstanceOf[Type[..$tparamNames]]
"""
def apply[..$tparamsNoVar](${valDef.name}: ${valDef.tpt}): Type[..$tparamNames] =
${valDef.name}.asInstanceOf[Type[..$tparamNames]]
"""
}
)
}
Expand Down
11 changes: 11 additions & 0 deletions shared/src/main/scala/io/estatico/newtype/macros/newsubtype.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.estatico.newtype.macros

import scala.annotation.StaticAnnotation

class newsubtype(
debug: Boolean = false,
debugRaw: Boolean = false
) extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro NewTypeMacros.newsubtypeAnnotation
}

Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,21 @@ class NewTypeMacrosTest extends FlatSpec with Matchers {
z shouldBe 4
assertCompiles("z: Id[Int]")
}

behavior of "@newsubtype"

it should "not box primitives" in {
// Introspect the runtime type returned by the `apply` method
def ctorReturnType(o: Any) = o.getClass.getMethods.find(_.getName == "apply").get.getReturnType

// newtypes will box primitive values.
@newtype case class BoxedInt(private val x: Int)
ctorReturnType(BoxedInt) shouldBe classOf[Object]

// newsubtypes will NOT box primitive values.
@newsubtype case class UnboxedInt(private val x: Int)
ctorReturnType(UnboxedInt) shouldBe classOf[Int]
}
}

object NewTypeMacrosTest {
Expand Down

0 comments on commit 2758733

Please sign in to comment.