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

Another attempt at Dotty cross-building #3486

Merged
merged 7 commits into from
Jun 19, 2020
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
9 changes: 9 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ jdk:

scala_version_212: &scala_version_212 2.12.11
scala_version_213: &scala_version_213 2.13.2
dotty_version: &dotty_version 0.24.0

before_install:
- export PATH=${PATH}:./vendor/bundle
Expand Down Expand Up @@ -76,6 +77,14 @@ jobs:
name: Binary compatibility 2.13
scala: *scala_version_213

# Note that we're currently only building some modules on Dotty, not running tests.
- &dotty_tests
stage: test
name: Dotty tests
env: TEST="Dotty tests"
script: sbt ++$TRAVIS_SCALA_VERSION! alleycatsLawsJVM/compile
scala: *dotty_version

- stage: styling
name: Linting
env: TEST="linting"
Expand Down
99 changes: 70 additions & 29 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ isTravisBuild in Global := sys.env.get("TRAVIS").isDefined

val scalaCheckVersion = "1.14.3"

val scalatestplusScalaCheckVersion = "3.1.2.0"
val scalatestVersion = "3.2.0"
val scalatestplusScalaCheckVersion = "3.2.0.0"

val disciplineVersion = "1.0.2"

Expand All @@ -43,9 +44,9 @@ def scalaVersionSpecificFolders(srcName: String, srcBaseDir: java.io.File, scala
List(CrossType.Pure, CrossType.Full)
.flatMap(_.sharedSrcDir(srcBaseDir, srcName).toList.map(f => file(f.getPath + suffix)))
CrossVersion.partialVersion(scalaVersion) match {
case Some((2, y)) if y >= 13 =>
extraDirs("-2.13+")
case _ => Nil
case Some((2, y)) => extraDirs("-2.x") ++ (if (y >= 13) extraDirs("-2.13+") else Nil)
case Some((0, _)) => extraDirs("-2.13+") ++ extraDirs("-3.x")
case _ => Nil
}
}

Expand All @@ -58,8 +59,11 @@ commonScalaVersionSettings

ThisBuild / mimaFailOnNoPrevious := false

def doctestGenTestsDottyCompat(isDotty: Boolean, genTests: Seq[File]): Seq[File] =
if (isDotty) Nil else genTests

lazy val commonSettings = commonScalaVersionSettings ++ Seq(
scalacOptions ++= commonScalacOptions(scalaVersion.value),
scalacOptions ++= commonScalacOptions(scalaVersion.value, isDotty.value),
Compile / unmanagedSourceDirectories ++= scalaVersionSpecificFolders("main", baseDirectory.value, scalaVersion.value),
Test / unmanagedSourceDirectories ++= scalaVersionSpecificFolders("test", baseDirectory.value, scalaVersion.value),
resolvers ++= Seq(Resolver.sonatypeRepo("releases"), Resolver.sonatypeRepo("snapshots")),
Expand All @@ -68,19 +72,26 @@ lazy val commonSettings = commonScalaVersionSettings ++ Seq(
) ++ warnUnusedImport

def macroDependencies(scalaVersion: String) =
Seq("org.scala-lang" % "scala-reflect" % scalaVersion % Provided)
if (scalaVersion.startsWith("2")) Seq("org.scala-lang" % "scala-reflect" % scalaVersion % Provided) else Nil

lazy val catsSettings = Seq(
incOptions := incOptions.value.withLogRecompileOnMacro(false),
libraryDependencies ++= Seq(
compilerPlugin(("org.typelevel" %% "kind-projector" % kindProjectorVersion).cross(CrossVersion.full))
libraryDependencies ++= (
if (isDotty.value) Nil
else
Seq(
compilerPlugin(("org.typelevel" %% "kind-projector" % kindProjectorVersion).cross(CrossVersion.full))
)
) ++ macroDependencies(scalaVersion.value)
) ++ commonSettings ++ publishSettings ++ scoverageSettings ++ simulacrumSettings

lazy val simulacrumSettings = Seq(
addCompilerPlugin(scalafixSemanticdb),
scalacOptions ++= Seq(s"-P:semanticdb:targetroot:${baseDirectory.value}/target/.semanticdb", "-Yrangepos"),
libraryDependencies += "org.typelevel" %% "simulacrum-scalafix-annotations" % "0.5.0",
libraryDependencies ++= (if (isDotty.value) Nil else Seq(compilerPlugin(scalafixSemanticdb))),
scalacOptions ++= (
if (isDotty.value) Nil else Seq(s"-P:semanticdb:targetroot:${baseDirectory.value}/target/.semanticdb", "-Yrangepos")
),
libraryDependencies +=
("org.typelevel" %% "simulacrum-scalafix-annotations" % "0.5.0").withDottyCompat(scalaVersion.value),
Copy link
Member

Choose a reason for hiding this comment

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

Does this have to be in the runtime classpath?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, this isn't new to this PR, but it's a good point—I'll change this to Provided before 2.2.0, and make sure that we're doing the same thing with Simulacrum Scalafix as we were with Simulacrum (the macro annotation version) in this respect. But I think that should be a follow-up.

pomPostProcess := { (node: xml.Node) =>
new RuleTransformer(new RewriteRule {
override def transform(node: xml.Node): Seq[xml.Node] =
Expand Down Expand Up @@ -140,15 +151,25 @@ lazy val includeGeneratedSrc: Setting[_] = {
}

lazy val disciplineDependencies = Seq(
libraryDependencies ++= Seq("org.scalacheck" %%% "scalacheck" % scalaCheckVersion,
"org.typelevel" %%% "discipline-core" % disciplineVersion
)
libraryDependencies ++= Seq(
"org.scalacheck" %%% "scalacheck" % scalaCheckVersion,
"org.typelevel" %%% "discipline-core" % disciplineVersion
).map(_.withDottyCompat(scalaVersion.value))
)

lazy val testingDependencies = Seq(
libraryDependencies ++= Seq(
"org.typelevel" %%% "discipline-scalatest" % disciplineScalatestVersion % Test,
"org.scalatest" %%% "scalatest-shouldmatchers" % scalatestVersion % Test,
"org.scalatest" %%% "scalatest-funsuite" % scalatestVersion % Test,
"org.scalatestplus" %%% "scalacheck-1-14" % scalatestplusScalaCheckVersion % Test
),
libraryDependencies ++= Seq(
("org.typelevel" %%% "discipline-scalatest" % disciplineScalatestVersion % Test)
).map(
_.exclude("org.scalatestplus", "scalacheck-1-14_2.13")
.exclude("org.scalactic", "scalactic_2.13")
.exclude("org.scalatest", "scalatest_2.13")
.withDottyCompat(scalaVersion.value)
Comment on lines +166 to +172
Copy link
Member

Choose a reason for hiding this comment

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

I'm tentatively okay with merging this as-is, but only if we follow up on discipline-scalatest asap to update its dependency. Exclusion rules in published dependencies are dangerous on several levels.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What do you think about changing the exclusion to only apply for Dotty (which we’re not currently publishing anyway)? I think it’s likely I’ll be the one publishing discipline-scalatest, and I don’t really want to start doing that for every Dotty release since it’s just a test dependency.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@djspiewak Oh, nevermind, I forgot that this is only a test dependency (and that's configured here, so there's no danger of them accidentally missing an % Test at the use site), so I don't see any problem with keeping the exclusions as they are now.

Copy link
Member

Choose a reason for hiding this comment

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

Ah if it's only a test dependency then I'm okay with it for now.

)
)

Expand Down Expand Up @@ -394,6 +415,7 @@ lazy val docs = project
.settings(docSettings)
.settings(commonJvmSettings)
.settings(
crossScalaVersions := crossScalaVersions.value.init,
libraryDependencies ++= Seq(
"org.typelevel" %%% "discipline-scalatest" % disciplineScalatestVersion
),
Expand Down Expand Up @@ -486,7 +508,10 @@ lazy val kernel = crossProject(JSPlatform, JVMPlatform)
.settings(includeGeneratedSrc)
.jsSettings(commonJsSettings)
.jvmSettings(commonJvmSettings ++ mimaSettings("cats-kernel"))
.settings(libraryDependencies += "org.scalacheck" %%% "scalacheck" % scalaCheckVersion % Test)
.settings(
libraryDependencies += ("org.scalacheck" %%% "scalacheck" % scalaCheckVersion % Test)
.withDottyCompat(scalaVersion.value)
)

lazy val kernelLaws = crossProject(JSPlatform, JVMPlatform)
.in(file("kernel-laws"))
Expand All @@ -509,7 +534,18 @@ lazy val core = crossProject(JSPlatform, JVMPlatform)
.settings(catsSettings)
.settings(sourceGenerators in Compile += (sourceManaged in Compile).map(Boilerplate.gen).taskValue)
.settings(includeGeneratedSrc)
.settings(libraryDependencies += "org.scalacheck" %%% "scalacheck" % scalaCheckVersion % Test)
.settings(
libraryDependencies += ("org.scalacheck" %%% "scalacheck" % scalaCheckVersion % Test)
.withDottyCompat(scalaVersion.value),
doctestGenTests := doctestGenTestsDottyCompat(isDotty.value, doctestGenTests.value)
)
.settings(
scalacOptions in Compile :=
(scalacOptions in Compile).value.filter {
case "-Xfatal-warnings" if isDotty.value => false
case _ => true
}
)
.jsSettings(commonJsSettings)
.jvmSettings(commonJvmSettings ++ mimaSettings("cats-core"))

Expand Down Expand Up @@ -746,7 +782,8 @@ addCommandAlias("validateJVM", ";fmtCheck;buildJVM;bench/test;validateBC;makeMic
addCommandAlias("validateJS", ";catsJS/compile;testsJS/test;js/test")
addCommandAlias("validateKernelJS", "kernelLawsJS/test")
addCommandAlias("validateFreeJS", "freeJS/test") //separated due to memory constraint on travis
addCommandAlias("validate", ";clean;validateJS;validateKernelJS;validateFreeJS;validateJVM")
addCommandAlias("validateDotty", ";++0.24.0!;alleycatsLawsJVM/compile")
addCommandAlias("validate", ";clean;validateJS;validateKernelJS;validateFreeJS;validateJVM;validateDotty")

addCommandAlias("prePR", "fmt")

Expand All @@ -773,29 +810,33 @@ lazy val crossVersionSharedSources: Seq[Setting[_]] =
}
}

def commonScalacOptions(scalaVersion: String) =
def commonScalacOptions(scalaVersion: String, isDotty: Boolean) =
Copy link
Member

Choose a reason for hiding this comment

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

As an fyi, the isDotty setting is computed by string matching on scalaVersion. Though, it's not exposed as such, so there may be no way we can take advantage of it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, I just thought this was clearer than trying to figure out and replicate whatever logic sbt-dotty uses for isDotty, but I have no objection to using .startsWith("0") or something similar instead.

Seq(
"-encoding",
"UTF-8",
"-feature",
"-language:existentials",
"-language:higherKinds",
"-language:implicitConversions",
"-unchecked",
"-Ywarn-dead-code",
"-Ywarn-numeric-widen",
"-Ywarn-value-discard",
"-Xfatal-warnings",
"-deprecation",
"-Xlint:-unused,_"
"-deprecation"
) ++ (if (priorTo2_13(scalaVersion))
Seq(
"-Yno-adapted-args",
"-Ypartial-unification",
"-Xfuture"
)
else
Nil)
Nil) ++ (if (isDotty)
Seq("-language:implicitConversions", "-Ykind-projector", "-Xignore-scala2-macros")
else
Seq(
"-language:existentials",
"-language:higherKinds",
"-language:implicitConversions",
"-Ywarn-dead-code",
"-Ywarn-numeric-widen",
"-Ywarn-value-discard",
"-Xlint:-unused,_"
))

def priorTo2_13(scalaVersion: String): Boolean =
CrossVersion.partialVersion(scalaVersion) match {
Expand Down Expand Up @@ -836,7 +877,7 @@ lazy val sharedReleaseProcess = Seq(
)

lazy val warnUnusedImport = Seq(
scalacOptions ++= Seq("-Ywarn-unused:imports"),
scalacOptions ++= (if (isDotty.value) Nil else Seq("-Ywarn-unused:imports")),
scalacOptions in (Compile, console) ~= { _.filterNot(Set("-Ywarn-unused-import", "-Ywarn-unused:imports")) },
scalacOptions in (Test, console) := (scalacOptions in (Compile, console)).value
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package cats
package arrow

import scala.language.experimental.macros
import scala.reflect.macros.blackbox.Context

private[arrow] class FunctionKMacroMethods {

/**
* Lifts function `f` of `F[A] => G[A]` into a `FunctionK[F, G]`.
*
* {{{
* def headOption[A](list: List[A]): Option[A] = list.headOption
* val lifted: FunctionK[List, Option] = FunctionK.lift(headOption)
* }}}
*
* Note: This method has a macro implementation that returns a new
* `FunctionK` instance as follows:
*
* {{{
* new FunctionK[F, G] {
* def apply[A](fa: F[A]): G[A] = f(fa)
* }
* }}}
*
* Additionally, the type parameters on `f` must not be specified.
*/
def lift[F[_], G[_]](f: (F[α] => G[α]) forSome { type α }): FunctionK[F, G] =
macro FunctionKMacros.lift[F, G]
}

private[arrow] object FunctionKMacros {

def lift[F[_], G[_]](c: Context)(
f: c.Expr[(F[α] => G[α]) forSome { type α }]
)(implicit
evF: c.WeakTypeTag[F[_]],
evG: c.WeakTypeTag[G[_]]
): c.Expr[FunctionK[F, G]] =
c.Expr[FunctionK[F, G]](new Lifter[c.type](c).lift[F, G](f.tree))
// ^^note: extra space after c.type to appease scalastyle

private[this] class Lifter[C <: Context](val c: C) {
import c.universe._

def lift[F[_], G[_]](tree: Tree)(implicit
evF: c.WeakTypeTag[F[_]],
evG: c.WeakTypeTag[G[_]]
): Tree =
unblock(tree) match {
case q"($param) => $trans[..$typeArgs](${arg: Ident})" if param.name == arg.name =>
typeArgs
.collect { case tt: TypeTree => tt }
.find(tt => Option(tt.original).isDefined)
.foreach { param =>
c.abort(param.pos,
s"type parameter $param must not be supplied when lifting function $trans to FunctionK"
)
}

val F = punchHole(evF.tpe)
val G = punchHole(evG.tpe)

q"""
new _root_.cats.arrow.FunctionK[$F, $G] {
def apply[A](fa: $F[A]): $G[A] = $trans(fa)
}
"""
case other =>
c.abort(other.pos, s"Unexpected tree $other when lifting to FunctionK")
}

private[this] def unblock(tree: Tree): Tree =
tree match {
case Block(Nil, expr) => expr
case _ => tree
}

private[this] def punchHole(tpe: Type): Tree =
tpe match {
case PolyType(undet :: Nil, underlying: TypeRef) =>
val α = TypeName("α")
def rebind(typeRef: TypeRef): Tree =
if (typeRef.sym == undet) tq"$α"
else {
val args = typeRef.args.map {
case ref: TypeRef => rebind(ref)
case arg => tq"$arg"
}
tq"${typeRef.sym}[..$args]"
}
val rebound = rebind(underlying)
tq"""({type λ[$α] = $rebound})#λ"""
case TypeRef(pre, sym, Nil) =>
tq"$sym"
case _ =>
c.abort(c.enclosingPosition, s"Unexpected type $tpe when lifting to FunctionK")
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package cats
package arrow

private[arrow] class FunctionKMacroMethods
Loading