Skip to content
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

newsubtype #14

Merged
merged 3 commits into from
Mar 29, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
15 changes: 15 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ lazy val newtype = crossProject.in(file("."))
.settings(defaultSettings)
.settings(releasePublishSettings)
.settings(name := "newtype")
.settings(crossVersionSharedSources)

lazy val newtypeJVM = newtype.jvm
lazy val newtypeJS = newtype.js
Expand Down Expand Up @@ -110,3 +111,17 @@ lazy val defaultLibraryDependencies = libraryDependencies ++= Seq(
"org.scalacheck" %%% "scalacheck" % "1.13.4" % "test",
"org.scalatest" %%% "scalatest" % "3.0.0" % "test"
)

def scalaPartV = Def.setting(CrossVersion.partialVersion(scalaVersion.value))

lazy val crossVersionSharedSources: Seq[Setting[_]] =
Seq(Compile, Test).map { sc =>
(unmanagedSourceDirectories in sc) ++= {
(unmanagedSourceDirectories in sc).value.map { dir =>
scalaPartV.value match {
case Some((2, y)) if y == 10 => new File(dir.getPath + "_2.10")
case Some((2, y)) if y >= 11 => new File(dir.getPath + "_2.11+")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package io.estatico.newtype
import cats._
import cats.implicits._
import io.estatico.newtype.ops._
import io.estatico.newtype.macros.newtype
import io.estatico.newtype.macros.{newsubtype, newtype}
import org.scalatest.{FlatSpec, Matchers}

class NewTypeCatsTest extends FlatSpec with Matchers {
Expand Down Expand Up @@ -44,6 +44,16 @@ class NewTypeCatsTest extends FlatSpec with Matchers {
it should "work in the same scope in which it is defined" in {
testNelTypeAliasExpansion shouldBe testNelTypeAliasExpansionExpectedResult
}

"Monoid[Sum]" should "work" in {
Monoid[Sum].empty shouldBe 0
List(2, 3, 4).coerce[List[Sum]].combineAll shouldBe 9
}

"Monoid[Prod]" should "work" in {
Monoid[Prod].empty shouldBe 1
List(2, 3, 4).coerce[List[Prod]].combineAll shouldBe 24
}
}

object NewTypeCatsTest {
Expand All @@ -70,4 +80,20 @@ object NewTypeCatsTest {
} yield x + y

private val testNelTypeAliasExpansionExpectedResult = Nel.of(2, 3, 4, 6, 6, 9)

@newsubtype case class Sum(value: Int)
object Sum {
implicit val monoid: Monoid[Sum] = new Monoid[Sum] {
override def empty: Sum = Sum(0)
override def combine(x: Sum, y: Sum): Sum = Sum(x.value + y.value)
}
}

@newsubtype case class Prod(value: Int)
object Prod {
implicit val monoid: Monoid[Prod] = new Monoid[Prod] {
override def empty: Prod = Prod(1)
override def combine(x: Prod, y: Prod): Prod = Prod(x.value * y.value)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.estatico.newtype.macros

import org.scalatest.{FlatSpec, Matchers}

class NewTypeMacrosJVMTest extends FlatSpec with Matchers {

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]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,28 @@ import scala.reflect.macros.blackbox

//noinspection TypeAnnotation
@macrocompat.bundle
private[macros] class NewTypeMacros(val c: blackbox.Context) {
private[macros] class NewTypeMacros(val c: blackbox.Context)
extends NewTypeCompatMacros {

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 +67,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,22 +85,40 @@ 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]"
Copy link
Contributor

Choose a reason for hiding this comment

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

Wouldn't this lead to problems with type inference again? What do we gain from using = instead of <:?

Copy link
Member Author

Choose a reason for hiding this comment

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

I think this was the result of me thinking that the abstract type was causing boxing, but later I realized boxing was happening for a different reason (which you commented on as well -
scala/bug#10770).

Just added a commit to fix this and all looks good, nice catch!

}

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) ::
generateClassTag(classTagName, valDef, tparamsNoVar, tparamNames, subtype) ::
maybeGenerateApplyMethod(clsDef, valDef, tparamsNoVar, tparamNames) :::
maybeGenerateOpsDef(clsDef, valDef, tparamsNoVar, tparamNames) :::
generateCoercibleInstances(tparamsNoVar, tparamNames, tparamsWild) :::
Expand All @@ -103,32 +131,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 +176,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 Expand Up @@ -190,7 +223,7 @@ private[macros] class NewTypeMacros(val c: blackbox.Context) {
if (clsDef.tparams.isEmpty) {
List(
q"""
implicit final class Ops$$newtype(val $$this$$: Type) extends AnyVal {
implicit final class Ops$$newtype(val $$this$$: Type) extends $opsClsParent {
..$extensionMethods
}
""",
Expand Down Expand Up @@ -299,10 +332,12 @@ private[macros] class NewTypeMacros(val c: blackbox.Context) {

// The erasure of opaque newtypes is always Object.
def generateClassTag(
name: TermName, tparamsNoVar: List[TypeDef], tparamNames: List[TypeName]
name: TermName, valDef: ValDef, tparamsNoVar: List[TypeDef], tparamNames: List[TypeName],
subtype: Boolean
): Tree = {
val objectClassTag = q"$ClassTagObj(_root_.scala.Predef.classOf[$ObjectCls])"
if (tparamsNoVar.isEmpty) q"implicit val $name: $ClassTagCls[Type] = $objectClassTag"
else q"implicit def $name[..$tparamsNoVar]: $ClassTagCls[Type[..$tparamNames]] = $objectClassTag"
val classTagType = if (subtype) valDef.tpt else tq"$ObjectCls"
val myClassTag = q"$ClassTagObj(_root_.scala.Predef.classOf[$classTagType])"
if (tparamsNoVar.isEmpty) q"implicit val $name: $ClassTagCls[Type] = $myClassTag"
else q"implicit def $name[..$tparamsNoVar]: $ClassTagCls[Type[..$tparamNames]] = $myClassTag"
}
}
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
@@ -0,0 +1,29 @@
package io.estatico.newtype.macros

import scala.reflect.macros.blackbox

trait NewTypeCompatMacros {

val c: blackbox.Context

import c.universe._

/**
* In scala 2.10 we can't reliably use AnyVal due to the
* way it combines == methods. For instance -
* {{{
* trait Tag
* final class Ops(val x: Int with Tag) extends AnyVal
* }}}
* doesn't work in Scala 2.10 -
* {{{
* error: ambiguous reference to overloaded definition,
* both method == in class Object of type (x$1: AnyRef)Boolean
* and method == in class Int of type (x: Double)Boolean
* match argument types (Int with Tag) and expected result type Boolean
* }}}
* @return
*/
def opsClsParent: Symbol = typeOf[AnyRef].typeSymbol
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.estatico.newtype.macros

import scala.reflect.macros.blackbox

trait NewTypeCompatMacros {

val c: blackbox.Context

import c.universe._

def opsClsParent: Symbol = typeOf[AnyVal].typeSymbol
}