diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eab620f..f2e122b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: runs-on: ${{ matrix.platform }} strategy: matrix: - java: [11, 17] + java: [17, 21, 25] platform: [ubuntu-24.04] steps: - uses: actions/checkout@v7 @@ -37,5 +37,9 @@ jobs: - name: Set up sbt uses: sbt/setup-sbt@v1 + - name: Check formatting + run: sbt -mem 3000 scalafmtCheckAll + if: matrix.java == 21 + - name: Run tests run: sbt -mem 3000 "+test" diff --git a/.scalafmt.conf b/.scalafmt.conf index 1b8d920..519feb7 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -runner.dialect = scala212 +runner.dialect = scala213 fileOverride { "glob:**/src/main/scala-3/**" { runner.dialect = scala3 @@ -7,4 +7,4 @@ fileOverride { style = defaultWithAlign maxColumn = 120 -version = 3.5.9 \ No newline at end of file +version = 3.11.5 diff --git a/README.md b/README.md index 7b78da5..86bdbae 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This tiny project is a cfor macro inspired by the one originally implemented in [Typelevel spire](https://github.com/typelevel/spire/blob/master/macros/src/main/scala/spire/macros/Syntax.scala) -library. Supports Scala 2.12 and 2.13 and has zero dependencies. +library. Supports Scala 2.13 and 3 and has zero dependencies. ## Example @@ -46,9 +46,9 @@ with the same performance as a pure java for loop. ## Installation -Available on maven central for scala 2.12 and 2.13 +Available on maven central for scala 2.13 and 3 ```scala -libraryDependencies += "io.github.metarank" %% "cfor" % "0.2" +libraryDependencies += "io.github.metarank" %% "cfor" % "0.3" ``` ## Purpose diff --git a/build.sbt b/build.sbt index 8a5501b..031df24 100644 --- a/build.sbt +++ b/build.sbt @@ -1,12 +1,5 @@ -name := "cfor" - val cforVersion = "0.3" -scalaVersion := "2.13.10" -version := cforVersion - -sonatypeProfileName := "io.github.metarank" - def isScala2(scalaVersion: String): Boolean = CrossVersion.partialVersion(scalaVersion) match { case Some((2, _)) => true @@ -14,10 +7,10 @@ def isScala2(scalaVersion: String): Boolean = } lazy val sharedSettings = Seq( - crossScalaVersions := Seq("2.12.16", "2.13.10", "3.2.1"), + crossScalaVersions := Seq("2.13.18", "3.9.0"), organization := "io.github.metarank", version := cforVersion, - scalaVersion := "2.13.10", + scalaVersion := "2.13.18", scalacOptions ++= Seq("-feature", "-deprecation"), libraryDependencies ++= (if (isScala2(scalaVersion.value)) { Seq("org.scala-lang" % "scala-reflect" % scalaVersion.value) @@ -25,10 +18,18 @@ lazy val sharedSettings = Seq( Seq.empty[ModuleID] }), libraryDependencies ++= Seq( - "org.scalatest" %% "scalatest" % "3.2.14" % "test" + "org.scalatest" %% "scalatest" % "3.2.20" % "test" ) ) +lazy val root = (project in file(".")) + .settings(sharedSettings) + .settings(name := "cfor-root", publish / skip := true) + .aggregate(macros, benchmark) + lazy val macros = (project in file("macros")).settings(sharedSettings) -lazy val benchmark = (project in file("benchmark")).settings(sharedSettings).dependsOn(macros) +lazy val benchmark = (project in file("benchmark")) + .settings(sharedSettings) + .settings(name := "cfor-benchmark", publish / skip := true) + .dependsOn(macros) diff --git a/macros/build.sbt b/macros/build.sbt index 88227c2..b91ac86 100644 --- a/macros/build.sbt +++ b/macros/build.sbt @@ -2,7 +2,7 @@ name := "cfor" publishMavenStyle := true -publishTo := sonatypePublishToBundle.value +publishTo := localStaging.value licenses := Seq("APL2" -> url("http://www.apache.org/licenses/LICENSE-2.0.txt")) diff --git a/macros/src/main/scala-2/io/github/metarank/cfor/Syntax.scala b/macros/src/main/scala-2/io/github/metarank/cfor/Syntax.scala index 878c44f..bbf1a0b 100644 --- a/macros/src/main/scala-2/io/github/metarank/cfor/Syntax.scala +++ b/macros/src/main/scala-2/io/github/metarank/cfor/Syntax.scala @@ -52,7 +52,7 @@ class InlineUtil[C <: Context with Singleton](val c: C) { } case tt: TypeTree if tt.original != null => - //super.transform(TypeTree().setOriginal(transform(tt.original))) + // super.transform(TypeTree().setOriginal(transform(tt.original))) super.transform(setOrig(c)(TypeTree(), transform(tt.original))) case _ => super.transform(tree) @@ -216,7 +216,7 @@ object Syntax { isLiteral(step) match { case Some(k) if k > 0 => strideUpUntil(i, j, k) case Some(k) if k < 0 => strideDownUntil(i, j, -k) - case Some(k) => + case Some(k) => c.error(c.enclosingPosition, "zero stride") q"()" case None => @@ -228,7 +228,7 @@ object Syntax { isLiteral(step) match { case Some(k) if k > 0 => strideUpTo(i, j, k) case Some(k) if k < 0 => strideDownTo(i, j, -k) - case Some(k) => + case Some(k) => c.error(c.enclosingPosition, "zero stride") q"()" case None => diff --git a/macros/src/test/scala-2.13/io/github/metarank/cfor/Cfor213Test.scala b/macros/src/test/scala-2.13/io/github/metarank/cfor/Cfor213Test.scala deleted file mode 100644 index b74dbff..0000000 --- a/macros/src/test/scala-2.13/io/github/metarank/cfor/Cfor213Test.scala +++ /dev/null @@ -1,15 +0,0 @@ -package io.github.metarank.cfor - -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -import scala.collection.mutable - -class Cfor213Test extends AnyFlatSpec with Matchers { - it should "iterate over array with anonymous function" in { - val l = mutable.ListBuffer[Int]() - val arr = Array(0, 1, 2, 3, 4) - cfor(arr)(l.append) - l.toList shouldBe List(0, 1, 2, 3, 4) - } -} diff --git a/macros/src/test/scala-2/io/github/metarank/cfor/CforScala2Test.scala b/macros/src/test/scala-2/io/github/metarank/cfor/CforScala2Test.scala new file mode 100644 index 0000000..97e74f1 --- /dev/null +++ b/macros/src/test/scala-2/io/github/metarank/cfor/CforScala2Test.scala @@ -0,0 +1,31 @@ +package io.github.metarank.cfor + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +import scala.collection.mutable + +class CforScala2Test extends AnyFlatSpec with Matchers { + it should "iterate over array with anonymous function" in { + val l = mutable.ListBuffer[Int]() + val arr = Array(0, 1, 2, 3, 4) + cfor(arr)(l.append) + l.toList shouldBe List(0, 1, 2, 3, 4) + } + + // the scala 2 macro inlines the loop body, so a closure captures the mutable + // loop variable itself, exactly like a hand-written while loop does + it should "capture value in closure" in { + val b1 = collection.mutable.ArrayBuffer.empty[() => Int] + cfor(0)(_ < 3, _ + 1) { x => + b1 += (() => x) + } + val b2 = collection.mutable.ArrayBuffer[() => Int]() + var i = 0 + while (i < 3) { + b2 += (() => i) + i += 1 + } + b1.map(_.apply()).toList shouldBe b2.map(_.apply()).toList + } +} diff --git a/macros/src/test/scala-3/io/github/metarank/cfor/CforScala3Test.scala b/macros/src/test/scala-3/io/github/metarank/cfor/CforScala3Test.scala new file mode 100644 index 0000000..e14c009 --- /dev/null +++ b/macros/src/test/scala-3/io/github/metarank/cfor/CforScala3Test.scala @@ -0,0 +1,16 @@ +package io.github.metarank.cfor + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class CforScala3Test extends AnyFlatSpec with Matchers { + // the scala 3 macro binds the loop value per iteration, so a closure captures + // the value of the current iteration rather than the mutable loop variable + it should "capture value in closure" in { + val b1 = collection.mutable.ArrayBuffer.empty[() => Int] + cfor(0)(_ < 3, _ + 1) { x => + b1 += (() => x) + } + b1.map(_.apply()).toList shouldBe List(0, 1, 2) + } +} diff --git a/macros/src/test/scala/io/github/metarank/cfor/CforTest.scala b/macros/src/test/scala/io/github/metarank/cfor/CforTest.scala index 3ea771e..52dae83 100644 --- a/macros/src/test/scala/io/github/metarank/cfor/CforTest.scala +++ b/macros/src/test/scala/io/github/metarank/cfor/CforTest.scala @@ -49,11 +49,11 @@ class CforTest extends AnyFlatSpec with Matchers { } it should "functions with side effects function values in cfor" in { - val b = mutable.ArrayBuffer.empty[Int] - var v = 0 + val b = mutable.ArrayBuffer.empty[Int] + var v = 0 def test: Int => Boolean = { v += 1; _ < 3 } - def incr: Int => Int = { v += 10; _ + 1 } - def body: Int => Unit = { + def incr: Int => Int = { v += 10; _ + 1 } + def body: Int => Unit = { v += 100 x => { b += x @@ -83,20 +83,6 @@ class CforTest extends AnyFlatSpec with Matchers { b.toList shouldBe List(0, 1, 2) } - it should "capture value in closure" in { - val b1 = collection.mutable.ArrayBuffer.empty[() => Int] - cfor(0)(_ < 3, _ + 1) { x => - b1 += (() => x) - } - val b2 = collection.mutable.ArrayBuffer[() => Int]() - var i = 0 - while (i < 3) { - b2 += (() => i) - i += 1 - } - b1.map(_.apply()).toList shouldBe b2.map(_.apply()).toList - } - it should "capture value in inner class" in { val b = collection.mutable.ArrayBuffer[Int]() cfor(0)(_ < 3, _ + 1) { x => @@ -144,7 +130,7 @@ class CforTest extends AnyFlatSpec with Matchers { it should "functions with side effects in cfor(arr)" in { var sum = 0 - var v = 0 + var v = 0 val arr = Array(0, 1, 2, 3, 4) cfor(arr) { v += 100 diff --git a/project/build.properties b/project/build.properties index ff2eef0..d3093be 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version = 1.8.0 \ No newline at end of file +sbt.version = 2.0.8 diff --git a/project/plugins.sbt b/project/plugins.sbt index 5113318..39dd2fc 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,4 +1,4 @@ -addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.3") -addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.15") -addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.1.2") -addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.4") +addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.8") +addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.3.2") +addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.7.0") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.6.2")