From ba2bf71d0e35231cc370f2b9fe1897f5203e67bc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 14:32:35 +0000 Subject: [PATCH 01/23] Initial plan From 391b464adc3371490dd0fd5e312218eee33b3bf2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 14:45:08 +0000 Subject: [PATCH 02/23] Add laws module with core infrastructure and initial tests Co-authored-by: Quafadas <24899792+Quafadas@users.noreply.github.com> --- laws/package.mill | 71 +++++++++++++++++++++++ laws/src/Dimension.scala | 28 +++++++++ laws/src/VectorCommutativeMonoid.scala | 57 +++++++++++++++++++ laws/src/VectorDimensionException.scala | 19 +++++++ laws/src/VectorMonoid.scala | 72 ++++++++++++++++++++++++ laws/src/instances/DoubleInstances.scala | 56 ++++++++++++++++++ laws/test/src/VectorMonoidLawsSpec.scala | 62 ++++++++++++++++++++ 7 files changed, 365 insertions(+) create mode 100644 laws/package.mill create mode 100644 laws/src/Dimension.scala create mode 100644 laws/src/VectorCommutativeMonoid.scala create mode 100644 laws/src/VectorDimensionException.scala create mode 100644 laws/src/VectorMonoid.scala create mode 100644 laws/src/instances/DoubleInstances.scala create mode 100644 laws/test/src/VectorMonoidLawsSpec.scala diff --git a/laws/package.mill b/laws/package.mill new file mode 100644 index 00000000..2a1e4674 --- /dev/null +++ b/laws/package.mill @@ -0,0 +1,71 @@ +package build.laws + +import mill.*, scalalib.*, scalajslib.*, scalanativelib.*, publish.* +import mill.scalajslib.api.ModuleKind +import com.goyeau.mill.scalafix.ScalafixModule +import mill.api.Task.Simple + +object `package` extends Module { + + val catsVersion = "2.10.0" + val disciplineVersion = "2.0.0" + val scalacheckVersion = "1.17.0" + + trait LawsModule extends PlatformScalaModule with build.VecxtPublishModule { + def mvnDeps = super.mvnDeps() ++ Seq( + mvn"org.typelevel::cats-kernel:$catsVersion" + ) + } + + object jvm extends LawsModule { + def moduleDeps = Seq(build.vecxt.jvm) + override def scalaVersion = build.V.scalaVersion + override def forkArgs = super.forkArgs() ++ build.vecIncubatorFlag + + object test extends ScalaTests, TestModule.Munit { + def moduleDeps = Seq(jvm, build.vecxt.jvm) + + def mvnDeps = super.mvnDeps() ++ Seq( + mvn"org.scalameta::munit::${build.V.munitVersion}", + mvn"org.typelevel::cats-kernel-laws:$catsVersion", + mvn"org.typelevel::discipline-munit:$disciplineVersion", + mvn"org.scalacheck::scalacheck:$scalacheckVersion" + ) + + override def forkArgs: Simple[Seq[String]] = super.forkArgs() ++ build.vecIncubatorFlag + } + } + + object js extends LawsModule with build.CommonJS { + def moduleDeps = Seq(build.vecxt.js) + def moduleKind = ModuleKind.ESModule + + object test extends ScalaJSTests, TestModule.Munit { + def moduleDeps = Seq(js, build.vecxt.js) + + def mvnDeps = super.mvnDeps() ++ Seq( + mvn"org.scalameta::munit::${build.V.munitVersion}", + mvn"org.typelevel::cats-kernel-laws:$catsVersion", + mvn"org.typelevel::discipline-munit:$disciplineVersion", + mvn"org.scalacheck::scalacheck:$scalacheckVersion" + ) + + def moduleKind = ModuleKind.CommonJSModule + } + } + + object native extends LawsModule with build.CommonNative { + def moduleDeps = Seq(build.vecxt.native) + + object test extends ScalaNativeTests, TestModule.Munit { + def moduleDeps = Seq(native, build.vecxt.native) + + def mvnDeps = super.mvnDeps() ++ Seq( + mvn"org.scalameta::munit::${build.V.munitVersion}", + mvn"org.typelevel::cats-kernel-laws:$catsVersion", + mvn"org.typelevel::discipline-munit:$disciplineVersion", + mvn"org.scalacheck::scalacheck:$scalacheckVersion" + ) + } + } +} diff --git a/laws/src/Dimension.scala b/laws/src/Dimension.scala new file mode 100644 index 00000000..7d264a02 --- /dev/null +++ b/laws/src/Dimension.scala @@ -0,0 +1,28 @@ +/* + * Copyright 2023 quafadas + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package vecxt.laws + +/** Dimension as an opaque type for type safety + */ +opaque type Dimension = Int + +object Dimension: + def apply(n: Int): Dimension = n + + extension (d: Dimension) + def size: Int = d +end Dimension diff --git a/laws/src/VectorCommutativeMonoid.scala b/laws/src/VectorCommutativeMonoid.scala new file mode 100644 index 00000000..01e30499 --- /dev/null +++ b/laws/src/VectorCommutativeMonoid.scala @@ -0,0 +1,57 @@ +/* + * Copyright 2023 quafadas + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package vecxt.laws + +import cats.kernel.{CommutativeMonoid, Semigroup} +import vecxt.BoundsCheck + +/** A CommutativeMonoid for Array[A] scoped to a specific dimension. + * + * This trait extends both VectorMonoid and cats.kernel.CommutativeMonoid, + * making it compatible with cats commutative monoid laws testing. + * + * @tparam A The element type (must form a Semigroup) + */ +trait VectorCommutativeMonoid[A] + extends VectorMonoid[A] with CommutativeMonoid[Array[A]] + +object VectorCommutativeMonoid: + /** Summon a VectorCommutativeMonoid instance for a specific dimension and type */ + def apply[A](using vcm: VectorCommutativeMonoid[A]): VectorCommutativeMonoid[A] = vcm + + /** Create a VectorCommutativeMonoid instance for a specific dimension + * + * @param dim The dimension for this monoid + * @param emptyFn Function to create the identity element + * @param combineFn Function to combine two arrays (must be commutative) + * @param bc BoundsCheck control for dimension validation + */ + def forDimension[A: Semigroup](dim: Dimension)( + emptyFn: => Array[A], + combineFn: (Array[A], Array[A]) => Array[A] + )(using bc: BoundsCheck.BoundsCheck = BoundsCheck.DoBoundsCheck.yes): VectorCommutativeMonoid[A] = + new VectorCommutativeMonoid[A]: + val dimension: Dimension = dim + + def empty = emptyFn + + def combine(x: Array[A], y: Array[A]) = + if bc == BoundsCheck.DoBoundsCheck.yes then + validateDim(x) + validateDim(y) + combineFn(x, y) +end VectorCommutativeMonoid diff --git a/laws/src/VectorDimensionException.scala b/laws/src/VectorDimensionException.scala new file mode 100644 index 00000000..490fba47 --- /dev/null +++ b/laws/src/VectorDimensionException.scala @@ -0,0 +1,19 @@ +/* + * Copyright 2023 quafadas + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package vecxt.laws + +class VectorDimensionException(msg: String) extends IllegalArgumentException(msg) diff --git a/laws/src/VectorMonoid.scala b/laws/src/VectorMonoid.scala new file mode 100644 index 00000000..c393e6f6 --- /dev/null +++ b/laws/src/VectorMonoid.scala @@ -0,0 +1,72 @@ +/* + * Copyright 2023 quafadas + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package vecxt.laws + +import cats.kernel.{Monoid, Semigroup} +import vecxt.BoundsCheck + +/** A Monoid for Array[A] scoped to a specific dimension. + * + * This trait extends cats.kernel.Monoid, making it compatible with + * the entire cats laws testing infrastructure. + * + * @tparam A The element type (must form a Semigroup) + */ +trait VectorMonoid[A] extends Monoid[Array[A]]: + /** The dimension this monoid operates on */ + def dimension: Dimension + + /** The identity element - an array of zeros of the correct dimension */ + def empty: Array[A] + + /** Combine two arrays element-wise */ + def combine(x: Array[A], y: Array[A]): Array[A] + + /** Validate that an array has the expected dimension */ + protected def validateDim(arr: Array[A]): Unit = + if arr.length != dimension.size then + throw new VectorDimensionException( + s"Expected dimension ${dimension.size}, got ${arr.length}" + ) +end VectorMonoid + +object VectorMonoid: + /** Summon a VectorMonoid instance for a specific dimension and type */ + def apply[A](using vm: VectorMonoid[A]): VectorMonoid[A] = vm + + /** Create a VectorMonoid instance for a specific dimension + * + * @param dim The dimension for this monoid + * @param emptyFn Function to create the identity element + * @param combineFn Function to combine two arrays + * @param bc BoundsCheck control for dimension validation + */ + def forDimension[A: Semigroup](dim: Dimension)( + emptyFn: => Array[A], + combineFn: (Array[A], Array[A]) => Array[A] + )(using bc: BoundsCheck.BoundsCheck = BoundsCheck.DoBoundsCheck.yes): VectorMonoid[A] = + new VectorMonoid[A]: + val dimension: Dimension = dim + + def empty = emptyFn + + def combine(x: Array[A], y: Array[A]) = + if bc == BoundsCheck.DoBoundsCheck.yes then + validateDim(x) + validateDim(y) + combineFn(x, y) +end VectorMonoid diff --git a/laws/src/instances/DoubleInstances.scala b/laws/src/instances/DoubleInstances.scala new file mode 100644 index 00000000..a44242e3 --- /dev/null +++ b/laws/src/instances/DoubleInstances.scala @@ -0,0 +1,56 @@ +/* + * Copyright 2023 quafadas + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package vecxt.laws.instances + +import cats.kernel.Semigroup +import vecxt.laws.{Dimension, VectorCommutativeMonoid} +import vecxt.BoundsCheck + +object double: + /** VectorCommutativeMonoid for Array[Double] with element-wise addition */ + def vectorAdditionMonoid(using dim: Dimension): VectorCommutativeMonoid[Double] = + given Semigroup[Double] = Semigroup.instance[Double](_ + _) + VectorCommutativeMonoid.forDimension(dim)( + emptyFn = Array.fill(dim.size)(0.0), + combineFn = (x, y) => { + val result = new Array[Double](x.length) + var i = 0 + while i < x.length do + result(i) = x(i) + y(i) + i += 1 + result + } + )(using Semigroup[Double], BoundsCheck.DoBoundsCheck.yes) + + /** VectorCommutativeMonoid for Array[Double] with element-wise multiplication + * + * Note: Element-wise multiplication is commutative + */ + def vectorMultiplicationMonoid(using dim: Dimension): VectorCommutativeMonoid[Double] = + given Semigroup[Double] = Semigroup.instance[Double](_ * _) + VectorCommutativeMonoid.forDimension(dim)( + emptyFn = Array.fill(dim.size)(1.0), + combineFn = (x, y) => { + val result = new Array[Double](x.length) + var i = 0 + while i < x.length do + result(i) = x(i) * y(i) + i += 1 + result + } + )(using Semigroup[Double], BoundsCheck.DoBoundsCheck.yes) +end double diff --git a/laws/test/src/VectorMonoidLawsSpec.scala b/laws/test/src/VectorMonoidLawsSpec.scala new file mode 100644 index 00000000..09accf40 --- /dev/null +++ b/laws/test/src/VectorMonoidLawsSpec.scala @@ -0,0 +1,62 @@ +/* + * Copyright 2023 quafadas + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package vecxt.laws + +import cats.kernel.laws.discipline.{MonoidTests, CommutativeMonoidTests} +import cats.kernel.Eq +import munit.DisciplineSuite +import org.scalacheck.{Arbitrary, Gen} +import vecxt.laws.{Dimension as LawsDimension} +import vecxt.laws.instances.double.* + +class VectorMonoidLawsSpec extends DisciplineSuite: + + /** Test Monoid laws for a specific dimension */ + def testMonoidLaws(n: Int): Unit = + // Create dimension witness + given testDim: LawsDimension = LawsDimension(n) + + // Create VectorCommutativeMonoid for this dimension + given VectorCommutativeMonoid[Double] = + vectorAdditionMonoid(using testDim) + + // Arbitrary generator for arrays of this dimension + given Arbitrary[Array[Double]] = Arbitrary( + Gen.listOfN(n, Arbitrary.arbitrary[Double]).map(_.toArray) + ) + + // Equality instance with tolerance for floating point comparisons + given Eq[Array[Double]] = Eq.instance((a, b) => + a.length == b.length && + a.zip(b).forall { case (x, y) => Math.abs(x - y) < 1e-6 } + ) + + // Test CommutativeMonoid laws (includes all Monoid laws plus commutativity) + checkAll( + s"VectorCommutativeMonoid[dim$n, Double].addition", + CommutativeMonoidTests[Array[Double]].commutativeMonoid + ) + end testMonoidLaws + + // Test various dimensions + testMonoidLaws(1) + testMonoidLaws(3) + testMonoidLaws(10) + testMonoidLaws(100) + testMonoidLaws(1000) + +end VectorMonoidLawsSpec From d3cabb7e949208cda9faae29509fb89c6de4eb25 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 14:49:32 +0000 Subject: [PATCH 03/23] Fix test Eq instance and verify JVM laws tests pass Co-authored-by: Quafadas <24899792+Quafadas@users.noreply.github.com> --- laws/package.mill | 8 +++++--- laws/test/src/VectorMonoidLawsSpec.scala | 13 ++++++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/laws/package.mill b/laws/package.mill index 2a1e4674..e0be168b 100644 --- a/laws/package.mill +++ b/laws/package.mill @@ -23,7 +23,7 @@ object `package` extends Module { override def forkArgs = super.forkArgs() ++ build.vecIncubatorFlag object test extends ScalaTests, TestModule.Munit { - def moduleDeps = Seq(jvm, build.vecxt.jvm) + def moduleDeps = Seq(jvm) def mvnDeps = super.mvnDeps() ++ Seq( mvn"org.scalameta::munit::${build.V.munitVersion}", @@ -41,7 +41,7 @@ object `package` extends Module { def moduleKind = ModuleKind.ESModule object test extends ScalaJSTests, TestModule.Munit { - def moduleDeps = Seq(js, build.vecxt.js) + def moduleDeps = Seq(js) def mvnDeps = super.mvnDeps() ++ Seq( mvn"org.scalameta::munit::${build.V.munitVersion}", @@ -57,8 +57,10 @@ object `package` extends Module { object native extends LawsModule with build.CommonNative { def moduleDeps = Seq(build.vecxt.native) + override def mvnDeps = super[LawsModule].mvnDeps() ++ super[CommonNative].mvnDeps() + object test extends ScalaNativeTests, TestModule.Munit { - def moduleDeps = Seq(native, build.vecxt.native) + def moduleDeps = Seq(native) def mvnDeps = super.mvnDeps() ++ Seq( mvn"org.scalameta::munit::${build.V.munitVersion}", diff --git a/laws/test/src/VectorMonoidLawsSpec.scala b/laws/test/src/VectorMonoidLawsSpec.scala index 09accf40..ce58ca84 100644 --- a/laws/test/src/VectorMonoidLawsSpec.scala +++ b/laws/test/src/VectorMonoidLawsSpec.scala @@ -35,14 +35,21 @@ class VectorMonoidLawsSpec extends DisciplineSuite: vectorAdditionMonoid(using testDim) // Arbitrary generator for arrays of this dimension + // Use smaller values to avoid floating point precision issues given Arbitrary[Array[Double]] = Arbitrary( - Gen.listOfN(n, Arbitrary.arbitrary[Double]).map(_.toArray) + Gen.listOfN(n, Gen.choose(-100.0, 100.0)).map(_.toArray) ) // Equality instance with tolerance for floating point comparisons given Eq[Array[Double]] = Eq.instance((a, b) => - a.length == b.length && - a.zip(b).forall { case (x, y) => Math.abs(x - y) < 1e-6 } + if a.length != b.length then false + else + var i = 0 + var equal = true + while i < a.length && equal do + equal = Math.abs(a(i) - b(i)) < 1e-10 + i += 1 + equal ) // Test CommutativeMonoid laws (includes all Monoid laws plus commutativity) From b7ba7e07b56652c11e2d660837aa2f5df4a0d03a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 14:50:46 +0000 Subject: [PATCH 04/23] Add comprehensive documentation for laws module Co-authored-by: Quafadas <24899792+Quafadas@users.noreply.github.com> --- laws/src/Dimension.scala | 6 +- laws/src/VectorCommutativeMonoid.scala | 42 +++-- laws/src/VectorMonoid.scala | 44 ++--- laws/src/instances/DoubleInstances.scala | 14 +- laws/test/src/VectorMonoidLawsSpec.scala | 17 +- site/docs/laws.md | 204 +++++++++++++++++++++++ 6 files changed, 273 insertions(+), 54 deletions(-) create mode 100644 site/docs/laws.md diff --git a/laws/src/Dimension.scala b/laws/src/Dimension.scala index 7d264a02..2a3b2dfa 100644 --- a/laws/src/Dimension.scala +++ b/laws/src/Dimension.scala @@ -22,7 +22,7 @@ opaque type Dimension = Int object Dimension: def apply(n: Int): Dimension = n - - extension (d: Dimension) - def size: Int = d + + extension (d: Dimension) def size: Int = d + end extension end Dimension diff --git a/laws/src/VectorCommutativeMonoid.scala b/laws/src/VectorCommutativeMonoid.scala index 01e30499..96386b86 100644 --- a/laws/src/VectorCommutativeMonoid.scala +++ b/laws/src/VectorCommutativeMonoid.scala @@ -20,38 +20,44 @@ import cats.kernel.{CommutativeMonoid, Semigroup} import vecxt.BoundsCheck /** A CommutativeMonoid for Array[A] scoped to a specific dimension. - * - * This trait extends both VectorMonoid and cats.kernel.CommutativeMonoid, - * making it compatible with cats commutative monoid laws testing. - * - * @tparam A The element type (must form a Semigroup) + * + * This trait extends both VectorMonoid and cats.kernel.CommutativeMonoid, making it compatible with cats commutative + * monoid laws testing. + * + * @tparam A + * The element type (must form a Semigroup) */ -trait VectorCommutativeMonoid[A] - extends VectorMonoid[A] with CommutativeMonoid[Array[A]] +trait VectorCommutativeMonoid[A] extends VectorMonoid[A] with CommutativeMonoid[Array[A]] object VectorCommutativeMonoid: /** Summon a VectorCommutativeMonoid instance for a specific dimension and type */ def apply[A](using vcm: VectorCommutativeMonoid[A]): VectorCommutativeMonoid[A] = vcm - + /** Create a VectorCommutativeMonoid instance for a specific dimension - * - * @param dim The dimension for this monoid - * @param emptyFn Function to create the identity element - * @param combineFn Function to combine two arrays (must be commutative) - * @param bc BoundsCheck control for dimension validation + * + * @param dim + * The dimension for this monoid + * @param emptyFn + * Function to create the identity element + * @param combineFn + * Function to combine two arrays (must be commutative) + * @param bc + * BoundsCheck control for dimension validation */ def forDimension[A: Semigroup](dim: Dimension)( - emptyFn: => Array[A], - combineFn: (Array[A], Array[A]) => Array[A] - )(using bc: BoundsCheck.BoundsCheck = BoundsCheck.DoBoundsCheck.yes): VectorCommutativeMonoid[A] = + emptyFn: => Array[A], + combineFn: (Array[A], Array[A]) => Array[A] + )(using bc: BoundsCheck.BoundsCheck = BoundsCheck.DoBoundsCheck.yes): VectorCommutativeMonoid[A] = new VectorCommutativeMonoid[A]: val dimension: Dimension = dim - + def empty = emptyFn - + def combine(x: Array[A], y: Array[A]) = if bc == BoundsCheck.DoBoundsCheck.yes then validateDim(x) validateDim(y) + end if combineFn(x, y) + end combine end VectorCommutativeMonoid diff --git a/laws/src/VectorMonoid.scala b/laws/src/VectorMonoid.scala index c393e6f6..ce58dbc5 100644 --- a/laws/src/VectorMonoid.scala +++ b/laws/src/VectorMonoid.scala @@ -20,22 +20,22 @@ import cats.kernel.{Monoid, Semigroup} import vecxt.BoundsCheck /** A Monoid for Array[A] scoped to a specific dimension. - * - * This trait extends cats.kernel.Monoid, making it compatible with - * the entire cats laws testing infrastructure. - * - * @tparam A The element type (must form a Semigroup) + * + * This trait extends cats.kernel.Monoid, making it compatible with the entire cats laws testing infrastructure. + * + * @tparam A + * The element type (must form a Semigroup) */ trait VectorMonoid[A] extends Monoid[Array[A]]: /** The dimension this monoid operates on */ def dimension: Dimension - + /** The identity element - an array of zeros of the correct dimension */ def empty: Array[A] - + /** Combine two arrays element-wise */ def combine(x: Array[A], y: Array[A]): Array[A] - + /** Validate that an array has the expected dimension */ protected def validateDim(arr: Array[A]): Unit = if arr.length != dimension.size then @@ -47,26 +47,32 @@ end VectorMonoid object VectorMonoid: /** Summon a VectorMonoid instance for a specific dimension and type */ def apply[A](using vm: VectorMonoid[A]): VectorMonoid[A] = vm - + /** Create a VectorMonoid instance for a specific dimension - * - * @param dim The dimension for this monoid - * @param emptyFn Function to create the identity element - * @param combineFn Function to combine two arrays - * @param bc BoundsCheck control for dimension validation + * + * @param dim + * The dimension for this monoid + * @param emptyFn + * Function to create the identity element + * @param combineFn + * Function to combine two arrays + * @param bc + * BoundsCheck control for dimension validation */ def forDimension[A: Semigroup](dim: Dimension)( - emptyFn: => Array[A], - combineFn: (Array[A], Array[A]) => Array[A] - )(using bc: BoundsCheck.BoundsCheck = BoundsCheck.DoBoundsCheck.yes): VectorMonoid[A] = + emptyFn: => Array[A], + combineFn: (Array[A], Array[A]) => Array[A] + )(using bc: BoundsCheck.BoundsCheck = BoundsCheck.DoBoundsCheck.yes): VectorMonoid[A] = new VectorMonoid[A]: val dimension: Dimension = dim - + def empty = emptyFn - + def combine(x: Array[A], y: Array[A]) = if bc == BoundsCheck.DoBoundsCheck.yes then validateDim(x) validateDim(y) + end if combineFn(x, y) + end combine end VectorMonoid diff --git a/laws/src/instances/DoubleInstances.scala b/laws/src/instances/DoubleInstances.scala index a44242e3..8206cbdf 100644 --- a/laws/src/instances/DoubleInstances.scala +++ b/laws/src/instances/DoubleInstances.scala @@ -26,31 +26,33 @@ object double: given Semigroup[Double] = Semigroup.instance[Double](_ + _) VectorCommutativeMonoid.forDimension(dim)( emptyFn = Array.fill(dim.size)(0.0), - combineFn = (x, y) => { + combineFn = (x, y) => val result = new Array[Double](x.length) var i = 0 while i < x.length do result(i) = x(i) + y(i) i += 1 + end while result - } )(using Semigroup[Double], BoundsCheck.DoBoundsCheck.yes) - + end vectorAdditionMonoid + /** VectorCommutativeMonoid for Array[Double] with element-wise multiplication - * + * * Note: Element-wise multiplication is commutative */ def vectorMultiplicationMonoid(using dim: Dimension): VectorCommutativeMonoid[Double] = given Semigroup[Double] = Semigroup.instance[Double](_ * _) VectorCommutativeMonoid.forDimension(dim)( emptyFn = Array.fill(dim.size)(1.0), - combineFn = (x, y) => { + combineFn = (x, y) => val result = new Array[Double](x.length) var i = 0 while i < x.length do result(i) = x(i) * y(i) i += 1 + end while result - } )(using Semigroup[Double], BoundsCheck.DoBoundsCheck.yes) + end vectorMultiplicationMonoid end double diff --git a/laws/test/src/VectorMonoidLawsSpec.scala b/laws/test/src/VectorMonoidLawsSpec.scala index ce58ca84..eacff732 100644 --- a/laws/test/src/VectorMonoidLawsSpec.scala +++ b/laws/test/src/VectorMonoidLawsSpec.scala @@ -20,26 +20,26 @@ import cats.kernel.laws.discipline.{MonoidTests, CommutativeMonoidTests} import cats.kernel.Eq import munit.DisciplineSuite import org.scalacheck.{Arbitrary, Gen} -import vecxt.laws.{Dimension as LawsDimension} +import vecxt.laws.Dimension as LawsDimension import vecxt.laws.instances.double.* class VectorMonoidLawsSpec extends DisciplineSuite: - + /** Test Monoid laws for a specific dimension */ def testMonoidLaws(n: Int): Unit = // Create dimension witness given testDim: LawsDimension = LawsDimension(n) - + // Create VectorCommutativeMonoid for this dimension - given VectorCommutativeMonoid[Double] = + given VectorCommutativeMonoid[Double] = vectorAdditionMonoid(using testDim) - + // Arbitrary generator for arrays of this dimension // Use smaller values to avoid floating point precision issues given Arbitrary[Array[Double]] = Arbitrary( Gen.listOfN(n, Gen.choose(-100.0, 100.0)).map(_.toArray) ) - + // Equality instance with tolerance for floating point comparisons given Eq[Array[Double]] = Eq.instance((a, b) => if a.length != b.length then false @@ -49,16 +49,17 @@ class VectorMonoidLawsSpec extends DisciplineSuite: while i < a.length && equal do equal = Math.abs(a(i) - b(i)) < 1e-10 i += 1 + end while equal ) - + // Test CommutativeMonoid laws (includes all Monoid laws plus commutativity) checkAll( s"VectorCommutativeMonoid[dim$n, Double].addition", CommutativeMonoidTests[Array[Double]].commutativeMonoid ) end testMonoidLaws - + // Test various dimensions testMonoidLaws(1) testMonoidLaws(3) diff --git a/site/docs/laws.md b/site/docs/laws.md new file mode 100644 index 00000000..0ddb041d --- /dev/null +++ b/site/docs/laws.md @@ -0,0 +1,204 @@ +# Laws Testing + +vecxt provides a laws-based testing infrastructure to verify algebraic properties of vector operations using [cats](https://typelevel.org/cats/) and [discipline](https://github.com/typelevel/discipline). + +## Overview + +Vector operations form algebraic structures (Monoids, Semigroups, etc.) that must satisfy certain mathematical laws. The vecxt laws module provides: + +- **Type-safe dimension tracking** via opaque types +- **Integration with cats laws** for automatic property testing +- **Cross-platform compatibility** (JVM, JS, Native) +- **Integration with vecxt's BoundsCheck system** + +## Why Laws Testing? + +Laws testing helps ensure that vector operations behave correctly by automatically checking properties like: + +- **Identity**: `combine(empty, x) === x` and `combine(x, empty) === x` +- **Associativity**: `combine(combine(x, y), z) === combine(x, combine(y, z))` +- **Commutativity** (for commutative operations): `combine(x, y) === combine(y, x)` + +These properties are verified across hundreds of generated test cases using property-based testing with ScalaCheck. + +## The Dimension Context Pattern + +Traditional Monoid typeclasses require a parameterless `empty` value: + +```scala +trait Monoid[A]: + def empty: A // ← How do we know what length this should be? + def combine(x: A, y: A): A +``` + +For vectors (backed by `Array[Double]`), the dimension is runtime information. We solve this using **dimension as implicit context**: + +```scala +// Dimension is an opaque type for type safety +opaque type Dimension = Int + +// VectorMonoid scoped to a specific dimension +trait VectorMonoid[A] extends Monoid[Array[A]]: + def dimension: Dimension + def empty: Array[A] + def combine(x: Array[A], y: Array[A]): Array[A] +``` + +## Usage + +### Setting Up a Dimension Context + +```scala +import vecxt.laws.* +import vecxt.laws.instances.double.* + +// Create a dimension witness +given dim: Dimension = Dimension(3) +``` + +### Creating Monoid Instances + +```scala +// Create a commutative monoid for vector addition +given VectorCommutativeMonoid[Double] = + vectorAdditionMonoid(using dim) + +// Or for multiplication +given VectorCommutativeMonoid[Double] = + vectorMultiplicationMonoid(using dim) +``` + +### Using in Computations + +```scala +def sumVectors(vectors: List[Array[Double]])(using vm: VectorMonoid[Double]): Array[Double] = + vectors.foldLeft(vm.empty)(vm.combine) + +// Usage +val vectors = List( + Array(1.0, 2.0, 3.0), + Array(4.0, 5.0, 6.0), + Array(7.0, 8.0, 9.0) +) + +val result = sumVectors(vectors) +// result: Array(12.0, 15.0, 18.0) +``` + +## Testing Your Own Operations + +You can test custom vector operations by creating your own VectorMonoid instances: + +```scala +import cats.kernel.Semigroup +import vecxt.laws.{Dimension, VectorCommutativeMonoid} +import vecxt.BoundsCheck + +def customVectorMonoid(using dim: Dimension): VectorCommutativeMonoid[Double] = + given Semigroup[Double] = Semigroup.instance[Double](_ + _) + VectorCommutativeMonoid.forDimension(dim)( + emptyFn = Array.fill(dim.size)(0.0), + combineFn = (x, y) => { + // Your custom combination logic + val result = new Array[Double](x.length) + var i = 0 + while i < x.length do + result(i) = x(i) + y(i) + i += 1 + result + } + )(using Semigroup[Double], BoundsCheck.DoBoundsCheck.yes) +``` + +Then test it with discipline: + +```scala +import cats.kernel.laws.discipline.CommutativeMonoidTests +import cats.kernel.Eq +import munit.DisciplineSuite +import org.scalacheck.{Arbitrary, Gen} + +class CustomMonoidLawsSpec extends DisciplineSuite: + given dim: Dimension = Dimension(10) + + given VectorCommutativeMonoid[Double] = customVectorMonoid + + given Arbitrary[Array[Double]] = Arbitrary( + Gen.listOfN(10, Gen.choose(-100.0, 100.0)).map(_.toArray) + ) + + given Eq[Array[Double]] = Eq.instance((a, b) => + if a.length != b.length then false + else + var i = 0 + var equal = true + while i < a.length && equal do + equal = Math.abs(a(i) - b(i)) < 1e-10 + i += 1 + equal + ) + + checkAll( + "CustomVectorMonoid", + CommutativeMonoidTests[Array[Double]].commutativeMonoid + ) +``` + +## Available Laws Tests + +The framework automatically tests: + +### Monoid Laws +- `combine(empty, x) === x` (left identity) +- `combine(x, empty) === x` (right identity) +- `combine(combine(x, y), z) === combine(x, combine(y, z))` (associativity) +- `combineAll` correctness +- `combineN` correctness +- `repeat0` returns `empty` +- `collect0` returns `empty` +- `isEmpty` detects identity element + +### Commutative Monoid Laws +All Monoid laws plus: +- `combine(x, y) === combine(y, x)` (commutativity) +- Intercalate operations preserve commutativity +- Reverse operations preserve commutativity + +## Benefits + +✅ **Correctness**: Automatically verify that operations satisfy mathematical laws +✅ **Property-Based Testing**: Tests with hundreds of generated inputs +✅ **Regression Prevention**: Catch bugs when refactoring implementations +✅ **Documentation**: Laws serve as executable specification +✅ **Integration**: Works with cats ecosystem and discipline +✅ **Zero Overhead**: Dimension validation can be disabled via BoundsCheck + +## Dependencies + +To use the laws module, add to your build: + +```scala +// Mill +def mvnDeps = Seq( + mvn"io.github.quafadas::vecxt-laws:$vecxtVersion" +) + +// For testing +def testMvnDeps = Seq( + mvn"org.scalameta::munit::$munitVersion", + mvn"org.typelevel::discipline-munit:$disciplineVersion", + mvn"org.scalacheck::scalacheck:$scalacheckVersion" +) +``` + +## Platform Support + +- ✅ **JVM**: Full support with comprehensive tests +- ✅ **JavaScript**: Compiles successfully (test execution pending) +- ✅ **Native**: Compiles successfully (test execution pending) + +## See Also + +- [cats kernel](https://typelevel.org/cats/typeclasses.html) +- [discipline](https://github.com/typelevel/discipline) - Law checking for type classes +- [vecxt BoundsCheck system](bounds.md) From 486ae5295135e4e75cedfcc6f6e11daed33883ab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 14:53:28 +0000 Subject: [PATCH 05/23] Address code review feedback: extract constants and givens Co-authored-by: Quafadas <24899792+Quafadas@users.noreply.github.com> --- laws/src/instances/DoubleInstances.scala | 10 ++++++---- laws/test/src/VectorMonoidLawsSpec.scala | 11 ++++++++--- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/laws/src/instances/DoubleInstances.scala b/laws/src/instances/DoubleInstances.scala index 8206cbdf..c151d299 100644 --- a/laws/src/instances/DoubleInstances.scala +++ b/laws/src/instances/DoubleInstances.scala @@ -21,9 +21,12 @@ import vecxt.laws.{Dimension, VectorCommutativeMonoid} import vecxt.BoundsCheck object double: + // Semigroup instances for Double operations + given additionSemigroup: Semigroup[Double] = Semigroup.instance[Double](_ + _) + given multiplicationSemigroup: Semigroup[Double] = Semigroup.instance[Double](_ * _) + /** VectorCommutativeMonoid for Array[Double] with element-wise addition */ def vectorAdditionMonoid(using dim: Dimension): VectorCommutativeMonoid[Double] = - given Semigroup[Double] = Semigroup.instance[Double](_ + _) VectorCommutativeMonoid.forDimension(dim)( emptyFn = Array.fill(dim.size)(0.0), combineFn = (x, y) => @@ -34,7 +37,7 @@ object double: i += 1 end while result - )(using Semigroup[Double], BoundsCheck.DoBoundsCheck.yes) + )(using additionSemigroup, BoundsCheck.DoBoundsCheck.yes) end vectorAdditionMonoid /** VectorCommutativeMonoid for Array[Double] with element-wise multiplication @@ -42,7 +45,6 @@ object double: * Note: Element-wise multiplication is commutative */ def vectorMultiplicationMonoid(using dim: Dimension): VectorCommutativeMonoid[Double] = - given Semigroup[Double] = Semigroup.instance[Double](_ * _) VectorCommutativeMonoid.forDimension(dim)( emptyFn = Array.fill(dim.size)(1.0), combineFn = (x, y) => @@ -53,6 +55,6 @@ object double: i += 1 end while result - )(using Semigroup[Double], BoundsCheck.DoBoundsCheck.yes) + )(using multiplicationSemigroup, BoundsCheck.DoBoundsCheck.yes) end vectorMultiplicationMonoid end double diff --git a/laws/test/src/VectorMonoidLawsSpec.scala b/laws/test/src/VectorMonoidLawsSpec.scala index eacff732..82a5946a 100644 --- a/laws/test/src/VectorMonoidLawsSpec.scala +++ b/laws/test/src/VectorMonoidLawsSpec.scala @@ -25,6 +25,11 @@ import vecxt.laws.instances.double.* class VectorMonoidLawsSpec extends DisciplineSuite: + // Test constants for floating-point comparisons + private val MinTestValue = -100.0 + private val MaxTestValue = 100.0 + private val FloatingPointTolerance = 1e-10 + /** Test Monoid laws for a specific dimension */ def testMonoidLaws(n: Int): Unit = // Create dimension witness @@ -35,9 +40,9 @@ class VectorMonoidLawsSpec extends DisciplineSuite: vectorAdditionMonoid(using testDim) // Arbitrary generator for arrays of this dimension - // Use smaller values to avoid floating point precision issues + // Use bounded values to avoid floating point precision issues given Arbitrary[Array[Double]] = Arbitrary( - Gen.listOfN(n, Gen.choose(-100.0, 100.0)).map(_.toArray) + Gen.listOfN(n, Gen.choose(MinTestValue, MaxTestValue)).map(_.toArray) ) // Equality instance with tolerance for floating point comparisons @@ -47,7 +52,7 @@ class VectorMonoidLawsSpec extends DisciplineSuite: var i = 0 var equal = true while i < a.length && equal do - equal = Math.abs(a(i) - b(i)) < 1e-10 + equal = Math.abs(a(i) - b(i)) < FloatingPointTolerance i += 1 end while equal From 5fd30f43a0fdef719f9cb1303f596b964e8a3592 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 15:10:18 +0000 Subject: [PATCH 06/23] Use vecxt extension methods in laws and add laws tests to CI Co-authored-by: Quafadas <24899792+Quafadas@users.noreply.github.com> --- .github/workflows/ci.yml | 4 ++++ laws/src/instances/DoubleInstances.scala | 27 ++++++++++-------------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d5e74d0..302cb783 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,6 +70,10 @@ jobs: - name: Test run: ./millw vecxt.${{ matrix.project }}.test + + - name: Laws Test + run: ./millw laws.${{ matrix.project }}.test + - name: Doc Gen run: ./millw site.siteGen diff --git a/laws/src/instances/DoubleInstances.scala b/laws/src/instances/DoubleInstances.scala index c151d299..459090ac 100644 --- a/laws/src/instances/DoubleInstances.scala +++ b/laws/src/instances/DoubleInstances.scala @@ -19,42 +19,37 @@ package vecxt.laws.instances import cats.kernel.Semigroup import vecxt.laws.{Dimension, VectorCommutativeMonoid} import vecxt.BoundsCheck +import vecxt.all.{given, *} object double: // Semigroup instances for Double operations given additionSemigroup: Semigroup[Double] = Semigroup.instance[Double](_ + _) given multiplicationSemigroup: Semigroup[Double] = Semigroup.instance[Double](_ * _) - /** VectorCommutativeMonoid for Array[Double] with element-wise addition */ + /** VectorCommutativeMonoid for Array[Double] with element-wise addition + * + * Uses vecxt's optimized array addition operator which leverages SIMD on JVM + */ def vectorAdditionMonoid(using dim: Dimension): VectorCommutativeMonoid[Double] = VectorCommutativeMonoid.forDimension(dim)( emptyFn = Array.fill(dim.size)(0.0), combineFn = (x, y) => - val result = new Array[Double](x.length) - var i = 0 - while i < x.length do - result(i) = x(i) + y(i) - i += 1 - end while - result + import vecxt.BoundsCheck.DoBoundsCheck.yes + x + y )(using additionSemigroup, BoundsCheck.DoBoundsCheck.yes) end vectorAdditionMonoid /** VectorCommutativeMonoid for Array[Double] with element-wise multiplication * - * Note: Element-wise multiplication is commutative + * Uses vecxt's optimized array multiplication operator which leverages SIMD on JVM Note: Element-wise multiplication + * is commutative */ def vectorMultiplicationMonoid(using dim: Dimension): VectorCommutativeMonoid[Double] = VectorCommutativeMonoid.forDimension(dim)( emptyFn = Array.fill(dim.size)(1.0), combineFn = (x, y) => - val result = new Array[Double](x.length) - var i = 0 - while i < x.length do - result(i) = x(i) * y(i) - i += 1 - end while - result + import vecxt.BoundsCheck.DoBoundsCheck.yes + x * y )(using multiplicationSemigroup, BoundsCheck.DoBoundsCheck.yes) end vectorMultiplicationMonoid end double From 1cb636bc922b0f7b0d05068062079423fa951347 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 15:26:54 +0000 Subject: [PATCH 07/23] Fix CI: only run laws tests on JVM platform Co-authored-by: Quafadas <24899792+Quafadas@users.noreply.github.com> --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 302cb783..ac80f66f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,6 +72,7 @@ jobs: run: ./millw vecxt.${{ matrix.project }}.test - name: Laws Test + if: matrix.project == 'jvm' run: ./millw laws.${{ matrix.project }}.test - name: Doc Gen From a92d5658464ae3584931628aa3787f07250f7c30 Mon Sep 17 00:00:00 2001 From: Simon Parten Date: Mon, 22 Dec 2025 11:41:15 +0000 Subject: [PATCH 08/23] Add multiplicative laws --- laws/package.mill | 3 +- laws/src/VectorMonoid.scala | 4 +- laws/src/instances/DoubleInstances.scala | 7 +- ...ala => VectorMonoidAdditiveLawsSpec.scala} | 11 +- .../VectorMonoidMultiplicativeLawsSpec.scala | 80 +++++ mill | 327 ++++++++++++++++++ vecxt/package.mill | 2 + 7 files changed, 422 insertions(+), 12 deletions(-) rename laws/test/src/{VectorMonoidLawsSpec.scala => VectorMonoidAdditiveLawsSpec.scala} (91%) create mode 100644 laws/test/src/VectorMonoidMultiplicativeLawsSpec.scala create mode 100755 mill diff --git a/laws/package.mill b/laws/package.mill index e0be168b..855eec97 100644 --- a/laws/package.mill +++ b/laws/package.mill @@ -39,6 +39,7 @@ object `package` extends Module { object js extends LawsModule with build.CommonJS { def moduleDeps = Seq(build.vecxt.js) def moduleKind = ModuleKind.ESModule + override def enableBsp = false object test extends ScalaJSTests, TestModule.Munit { def moduleDeps = Seq(js) @@ -56,7 +57,7 @@ object `package` extends Module { object native extends LawsModule with build.CommonNative { def moduleDeps = Seq(build.vecxt.native) - + override def enableBsp = false override def mvnDeps = super[LawsModule].mvnDeps() ++ super[CommonNative].mvnDeps() object test extends ScalaNativeTests, TestModule.Munit { diff --git a/laws/src/VectorMonoid.scala b/laws/src/VectorMonoid.scala index ce58dbc5..ac173f86 100644 --- a/laws/src/VectorMonoid.scala +++ b/laws/src/VectorMonoid.scala @@ -62,14 +62,14 @@ object VectorMonoid: def forDimension[A: Semigroup](dim: Dimension)( emptyFn: => Array[A], combineFn: (Array[A], Array[A]) => Array[A] - )(using bc: BoundsCheck.BoundsCheck = BoundsCheck.DoBoundsCheck.yes): VectorMonoid[A] = + )(using bc: BoundsCheck.BoundsCheck): VectorMonoid[A] = new VectorMonoid[A]: val dimension: Dimension = dim def empty = emptyFn def combine(x: Array[A], y: Array[A]) = - if bc == BoundsCheck.DoBoundsCheck.yes then + if bc then validateDim(x) validateDim(y) end if diff --git a/laws/src/instances/DoubleInstances.scala b/laws/src/instances/DoubleInstances.scala index 459090ac..3fb1dd2d 100644 --- a/laws/src/instances/DoubleInstances.scala +++ b/laws/src/instances/DoubleInstances.scala @@ -22,9 +22,6 @@ import vecxt.BoundsCheck import vecxt.all.{given, *} object double: - // Semigroup instances for Double operations - given additionSemigroup: Semigroup[Double] = Semigroup.instance[Double](_ + _) - given multiplicationSemigroup: Semigroup[Double] = Semigroup.instance[Double](_ * _) /** VectorCommutativeMonoid for Array[Double] with element-wise addition * @@ -36,7 +33,7 @@ object double: combineFn = (x, y) => import vecxt.BoundsCheck.DoBoundsCheck.yes x + y - )(using additionSemigroup, BoundsCheck.DoBoundsCheck.yes) + ) end vectorAdditionMonoid /** VectorCommutativeMonoid for Array[Double] with element-wise multiplication @@ -50,6 +47,6 @@ object double: combineFn = (x, y) => import vecxt.BoundsCheck.DoBoundsCheck.yes x * y - )(using multiplicationSemigroup, BoundsCheck.DoBoundsCheck.yes) + ) end vectorMultiplicationMonoid end double diff --git a/laws/test/src/VectorMonoidLawsSpec.scala b/laws/test/src/VectorMonoidAdditiveLawsSpec.scala similarity index 91% rename from laws/test/src/VectorMonoidLawsSpec.scala rename to laws/test/src/VectorMonoidAdditiveLawsSpec.scala index 82a5946a..025d9b23 100644 --- a/laws/test/src/VectorMonoidLawsSpec.scala +++ b/laws/test/src/VectorMonoidAdditiveLawsSpec.scala @@ -20,6 +20,7 @@ import cats.kernel.laws.discipline.{MonoidTests, CommutativeMonoidTests} import cats.kernel.Eq import munit.DisciplineSuite import org.scalacheck.{Arbitrary, Gen} +import scala.util.Random import vecxt.laws.Dimension as LawsDimension import vecxt.laws.instances.double.* @@ -66,10 +67,12 @@ class VectorMonoidLawsSpec extends DisciplineSuite: end testMonoidLaws // Test various dimensions - testMonoidLaws(1) + testMonoidLaws(0) + testMonoidLaws(1) testMonoidLaws(3) - testMonoidLaws(10) - testMonoidLaws(100) - testMonoidLaws(1000) + + // Test three random dimensions between 5-1000 + private val randomDims = Random.shuffle((5 to 1000).toList).take(3) + randomDims.foreach(testMonoidLaws) end VectorMonoidLawsSpec diff --git a/laws/test/src/VectorMonoidMultiplicativeLawsSpec.scala b/laws/test/src/VectorMonoidMultiplicativeLawsSpec.scala new file mode 100644 index 00000000..4738c92a --- /dev/null +++ b/laws/test/src/VectorMonoidMultiplicativeLawsSpec.scala @@ -0,0 +1,80 @@ +/* + * Copyright 2023 quafadas + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package vecxt.laws + +import cats.kernel.laws.discipline.CommutativeMonoidTests +import cats.kernel.Eq +import munit.DisciplineSuite +import org.scalacheck.{Arbitrary, Gen} +import scala.util.Random +import vecxt.laws.Dimension as LawsDimension +import vecxt.laws.instances.double.* + +class VectorMultiplicativeMonoidLawsSpec extends DisciplineSuite: + + // Test constants for floating-point comparisons + // Use smaller range to avoid extreme floating-point precision issues in multiplication + private val MinTestValue = -10.0 + private val MaxTestValue = 10.0 + // Use more relaxed tolerance for multiplication operations due to accumulated rounding errors + private val FloatingPointTolerance = 1e-8 + + /** Test Monoid laws for a specific dimension */ + def testMonoidLaws(n: Int): Unit = + // Create dimension witness + given testDim: LawsDimension = LawsDimension(n) + + // Create VectorCommutativeMonoid for this dimension + given VectorCommutativeMonoid[Double] = + vectorMultiplicationMonoid(using testDim) + + // Arbitrary generator for arrays of this dimension + // Use bounded values to avoid floating point precision issues + given Arbitrary[Array[Double]] = Arbitrary( + Gen.listOfN(n, Gen.choose(MinTestValue, MaxTestValue)).map(_.toArray) + ) + + // Equality instance with tolerance for floating point comparisons + given Eq[Array[Double]] = Eq.instance((a, b) => + if a.length != b.length then false + else + var i = 0 + var equal = true + while i < a.length && equal do + equal = Math.abs(a(i) - b(i)) < FloatingPointTolerance + i += 1 + end while + equal + ) + + // Test CommutativeMonoid laws (includes all Monoid laws plus commutativity) + checkAll( + s"VectorCommutativeMonoid[dim$n, Double].multiplication", + CommutativeMonoidTests[Array[Double]].commutativeMonoid + ) + end testMonoidLaws + + // Test various dimensions + testMonoidLaws(0) + testMonoidLaws(1) + testMonoidLaws(3) + + // Test three random dimensions between 5-1000 + private val randomDims = Random.shuffle((5 to 1000).toList).take(3) + randomDims.foreach(testMonoidLaws) + + diff --git a/mill b/mill new file mode 100755 index 00000000..555d7fc7 --- /dev/null +++ b/mill @@ -0,0 +1,327 @@ +#!/usr/bin/env sh + +# This is a wrapper script, that automatically selects or downloads Mill from Maven Central or GitHub release pages. +# +# This script determines the Mill version to use by trying these sources +# - env-variable `MILL_VERSION` +# - local file `.mill-version` +# - local file `.config/mill-version` +# - `mill-version` from YAML fronmatter of current buildfile +# - if accessible, find the latest stable version available on Maven Central (https://repo1.maven.org/maven2) +# - env-variable `DEFAULT_MILL_VERSION` +# +# If a version has the suffix '-native' a native binary will be used. +# If a version has the suffix '-jvm' an executable jar file will be used, requiring an already installed Java runtime. +# If no such suffix is found, the script will pick a default based on version and platform. +# +# Once a version was determined, it tries to use either +# - a system-installed mill, if found and it's version matches +# - an already downloaded version under ~/.cache/mill/download +# +# If no working mill version was found on the system, +# this script downloads a binary file from Maven Central or Github Pages (this is version dependent) +# into a cache location (~/.cache/mill/download). +# +# Mill Project URL: https://github.com/com-lihaoyi/mill +# Script Version: 1.0.0-M1-49-ac90e3 +# +# If you want to improve this script, please also contribute your changes back! +# This script was generated from: dist/scripts/src/mill.sh +# +# Licensed under the Apache License, Version 2.0 + +set -e + +if [ -z "${DEFAULT_MILL_VERSION}" ] ; then + DEFAULT_MILL_VERSION=0.12.14 +fi + + +if [ -z "${GITHUB_RELEASE_CDN}" ] ; then + GITHUB_RELEASE_CDN="" +fi + + +MILL_REPO_URL="https://github.com/com-lihaoyi/mill" + +if [ -z "${CURL_CMD}" ] ; then + CURL_CMD=curl +fi + +# Explicit commandline argument takes precedence over all other methods +if [ "$1" = "--mill-version" ] ; then + echo "The --mill-version option is no longer supported." 1>&2 +fi + +MILL_BUILD_SCRIPT="" + +if [ -f "build.mill" ] ; then + MILL_BUILD_SCRIPT="build.mill" +elif [ -f "build.mill.scala" ] ; then + MILL_BUILD_SCRIPT="build.mill.scala" +elif [ -f "build.sc" ] ; then + MILL_BUILD_SCRIPT="build.sc" +fi + +# Please note, that if a MILL_VERSION is already set in the environment, +# We reuse it's value and skip searching for a value. + +# If not already set, read .mill-version file +if [ -z "${MILL_VERSION}" ] ; then + if [ -f ".mill-version" ] ; then + MILL_VERSION="$(tr '\r' '\n' < .mill-version | head -n 1 2> /dev/null)" + elif [ -f ".config/mill-version" ] ; then + MILL_VERSION="$(tr '\r' '\n' < .config/mill-version | head -n 1 2> /dev/null)" + elif [ -n "${MILL_BUILD_SCRIPT}" ] ; then + MILL_VERSION="$(cat ${MILL_BUILD_SCRIPT} | grep '//[|] *mill-version: *' | sed 's;//| *mill-version: *;;')" + fi +fi + +MILL_USER_CACHE_DIR="${XDG_CACHE_HOME:-${HOME}/.cache}/mill" + +if [ -z "${MILL_DOWNLOAD_PATH}" ] ; then + MILL_DOWNLOAD_PATH="${MILL_USER_CACHE_DIR}/download" +fi + +# If not already set, try to fetch newest from Github +if [ -z "${MILL_VERSION}" ] ; then + # TODO: try to load latest version from release page + echo "No mill version specified." 1>&2 + echo "You should provide a version via a '//| mill-version: ' comment or a '.mill-version' file." 1>&2 + + mkdir -p "${MILL_DOWNLOAD_PATH}" + LANG=C touch -d '1 hour ago' "${MILL_DOWNLOAD_PATH}/.expire_latest" 2>/dev/null || ( + # we might be on OSX or BSD which don't have -d option for touch + # but probably a -A [-][[hh]mm]SS + touch "${MILL_DOWNLOAD_PATH}/.expire_latest"; touch -A -010000 "${MILL_DOWNLOAD_PATH}/.expire_latest" + ) || ( + # in case we still failed, we retry the first touch command with the intention + # to show the (previously suppressed) error message + LANG=C touch -d '1 hour ago' "${MILL_DOWNLOAD_PATH}/.expire_latest" + ) + + # POSIX shell variant of bash's -nt operator, see https://unix.stackexchange.com/a/449744/6993 + # if [ "${MILL_DOWNLOAD_PATH}/.latest" -nt "${MILL_DOWNLOAD_PATH}/.expire_latest" ] ; then + if [ -n "$(find -L "${MILL_DOWNLOAD_PATH}/.latest" -prune -newer "${MILL_DOWNLOAD_PATH}/.expire_latest")" ]; then + # we know a current latest version + MILL_VERSION=$(head -n 1 "${MILL_DOWNLOAD_PATH}"/.latest 2> /dev/null) + fi + + if [ -z "${MILL_VERSION}" ] ; then + # we don't know a current latest version + echo "Retrieving latest mill version ..." 1>&2 + LANG=C ${CURL_CMD} -s -i -f -I ${MILL_REPO_URL}/releases/latest 2> /dev/null | grep --ignore-case Location: | sed s'/^.*tag\///' | tr -d '\r\n' > "${MILL_DOWNLOAD_PATH}/.latest" + MILL_VERSION=$(head -n 1 "${MILL_DOWNLOAD_PATH}"/.latest 2> /dev/null) + fi + + if [ -z "${MILL_VERSION}" ] ; then + # Last resort + MILL_VERSION="${DEFAULT_MILL_VERSION}" + echo "Falling back to hardcoded mill version ${MILL_VERSION}" 1>&2 + else + echo "Using mill version ${MILL_VERSION}" 1>&2 + fi +fi + +MILL_NATIVE_SUFFIX="-native" +MILL_JVM_SUFFIX="-jvm" +FULL_MILL_VERSION=$MILL_VERSION +ARTIFACT_SUFFIX="" +set_artifact_suffix(){ + if [ "$(expr substr $(uname -s) 1 5 2>/dev/null)" = "Linux" ]; then + if [ "$(uname -m)" = "aarch64" ]; then + ARTIFACT_SUFFIX="-native-linux-aarch64" + else + ARTIFACT_SUFFIX="-native-linux-amd64" + fi + elif [ "$(uname)" = "Darwin" ]; then + if [ "$(uname -m)" = "arm64" ]; then + ARTIFACT_SUFFIX="-native-mac-aarch64" + else + ARTIFACT_SUFFIX="-native-mac-amd64" + fi + else + echo "This native mill launcher supports only Linux and macOS." 1>&2 + exit 1 + fi +} + +case "$MILL_VERSION" in + *"$MILL_NATIVE_SUFFIX") + MILL_VERSION=${MILL_VERSION%"$MILL_NATIVE_SUFFIX"} + set_artifact_suffix + ;; + + *"$MILL_JVM_SUFFIX") + MILL_VERSION=${MILL_VERSION%"$MILL_JVM_SUFFIX"} + ;; + + *) + case "$MILL_VERSION" in + 0.1.*) ;; + 0.2.*) ;; + 0.3.*) ;; + 0.4.*) ;; + 0.5.*) ;; + 0.6.*) ;; + 0.7.*) ;; + 0.8.*) ;; + 0.9.*) ;; + 0.10.*) ;; + 0.11.*) ;; + 0.12.*) ;; + *) + set_artifact_suffix + esac + ;; +esac + +MILL="${MILL_DOWNLOAD_PATH}/$MILL_VERSION$ARTIFACT_SUFFIX" + +try_to_use_system_mill() { + if [ "$(uname)" != "Linux" ]; then + return 0 + fi + + MILL_IN_PATH="$(command -v mill || true)" + + if [ -z "${MILL_IN_PATH}" ]; then + return 0 + fi + + SYSTEM_MILL_FIRST_TWO_BYTES=$(head --bytes=2 "${MILL_IN_PATH}") + if [ "${SYSTEM_MILL_FIRST_TWO_BYTES}" = "#!" ]; then + # MILL_IN_PATH is (very likely) a shell script and not the mill + # executable, ignore it. + return 0 + fi + + SYSTEM_MILL_PATH=$(readlink -e "${MILL_IN_PATH}") + SYSTEM_MILL_SIZE=$(stat --format=%s "${SYSTEM_MILL_PATH}") + SYSTEM_MILL_MTIME=$(stat --format=%y "${SYSTEM_MILL_PATH}") + + if [ ! -d "${MILL_USER_CACHE_DIR}" ]; then + mkdir -p "${MILL_USER_CACHE_DIR}" + fi + + SYSTEM_MILL_INFO_FILE="${MILL_USER_CACHE_DIR}/system-mill-info" + if [ -f "${SYSTEM_MILL_INFO_FILE}" ]; then + parseSystemMillInfo() { + LINE_NUMBER="${1}" + # Select the line number of the SYSTEM_MILL_INFO_FILE, cut the + # variable definition in that line in two halves and return + # the value, and finally remove the quotes. + sed -n "${LINE_NUMBER}p" "${SYSTEM_MILL_INFO_FILE}" |\ + cut -d= -f2 |\ + sed 's/"\(.*\)"/\1/' + } + + CACHED_SYSTEM_MILL_PATH=$(parseSystemMillInfo 1) + CACHED_SYSTEM_MILL_VERSION=$(parseSystemMillInfo 2) + CACHED_SYSTEM_MILL_SIZE=$(parseSystemMillInfo 3) + CACHED_SYSTEM_MILL_MTIME=$(parseSystemMillInfo 4) + + if [ "${SYSTEM_MILL_PATH}" = "${CACHED_SYSTEM_MILL_PATH}" ] \ + && [ "${SYSTEM_MILL_SIZE}" = "${CACHED_SYSTEM_MILL_SIZE}" ] \ + && [ "${SYSTEM_MILL_MTIME}" = "${CACHED_SYSTEM_MILL_MTIME}" ]; then + if [ "${CACHED_SYSTEM_MILL_VERSION}" = "${MILL_VERSION}" ]; then + MILL="${SYSTEM_MILL_PATH}" + return 0 + else + return 0 + fi + fi + fi + + SYSTEM_MILL_VERSION=$(${SYSTEM_MILL_PATH} --version | head -n1 | sed -n 's/^Mill.*version \(.*\)/\1/p') + + cat < "${SYSTEM_MILL_INFO_FILE}" +CACHED_SYSTEM_MILL_PATH="${SYSTEM_MILL_PATH}" +CACHED_SYSTEM_MILL_VERSION="${SYSTEM_MILL_VERSION}" +CACHED_SYSTEM_MILL_SIZE="${SYSTEM_MILL_SIZE}" +CACHED_SYSTEM_MILL_MTIME="${SYSTEM_MILL_MTIME}" +EOF + + if [ "${SYSTEM_MILL_VERSION}" = "${MILL_VERSION}" ]; then + MILL="${SYSTEM_MILL_PATH}" + fi +} +try_to_use_system_mill + +# If not already downloaded, download it +if [ ! -s "${MILL}" ] || [ "$MILL_TEST_DRY_RUN_LAUNCHER_SCRIPT" = "1" ] ; then + case $MILL_VERSION in + 0.0.* | 0.1.* | 0.2.* | 0.3.* | 0.4.* ) + DOWNLOAD_SUFFIX="" + DOWNLOAD_FROM_MAVEN=0 + ;; + 0.5.* | 0.6.* | 0.7.* | 0.8.* | 0.9.* | 0.10.* | 0.11.0-M* ) + DOWNLOAD_SUFFIX="-assembly" + DOWNLOAD_FROM_MAVEN=0 + ;; + *) + DOWNLOAD_SUFFIX="-assembly" + DOWNLOAD_FROM_MAVEN=1 + ;; + esac + case $MILL_VERSION in + 0.12.0 | 0.12.1 | 0.12.2 | 0.12.3 | 0.12.4 | 0.12.5 | 0.12.6 | 0.12.7 | 0.12.8 | 0.12.9 | 0.12.10 | 0.12.11 ) + DOWNLOAD_EXT="jar" + ;; + 0.12.* ) + DOWNLOAD_EXT="exe" + ;; + 0.* ) + DOWNLOAD_EXT="jar" + ;; + *) + DOWNLOAD_EXT="exe" + ;; + esac + + DOWNLOAD_FILE=$(mktemp mill.XXXXXX) + if [ "$DOWNLOAD_FROM_MAVEN" = "1" ] ; then + DOWNLOAD_URL="https://repo1.maven.org/maven2/com/lihaoyi/mill-dist${ARTIFACT_SUFFIX}/${MILL_VERSION}/mill-dist${ARTIFACT_SUFFIX}-${MILL_VERSION}.${DOWNLOAD_EXT}" + else + MILL_VERSION_TAG=$(echo "$MILL_VERSION" | sed -E 's/([^-]+)(-M[0-9]+)?(-.*)?/\1\2/') + DOWNLOAD_URL="${GITHUB_RELEASE_CDN}${MILL_REPO_URL}/releases/download/${MILL_VERSION_TAG}/${MILL_VERSION}${DOWNLOAD_SUFFIX}" + unset MILL_VERSION_TAG + fi + + if [ "$MILL_TEST_DRY_RUN_LAUNCHER_SCRIPT" = "1" ] ; then + echo $DOWNLOAD_URL + echo $MILL + exit 0 + fi + # TODO: handle command not found + echo "Downloading mill ${MILL_VERSION} from ${DOWNLOAD_URL} ..." 1>&2 + ${CURL_CMD} -f -L -o "${DOWNLOAD_FILE}" "${DOWNLOAD_URL}" + chmod +x "${DOWNLOAD_FILE}" + mkdir -p "${MILL_DOWNLOAD_PATH}" + mv "${DOWNLOAD_FILE}" "${MILL}" + + unset DOWNLOAD_FILE + unset DOWNLOAD_SUFFIX +fi + +if [ -z "$MILL_MAIN_CLI" ] ; then + MILL_MAIN_CLI="${0}" +fi + +MILL_FIRST_ARG="" +if [ "$1" = "--bsp" ] || [ "$1" = "-i" ] || [ "$1" = "--interactive" ] || [ "$1" = "--no-server" ] || [ "$1" = "--repl" ] || [ "$1" = "--help" ] ; then + # Need to preserve the first position of those listed options + MILL_FIRST_ARG=$1 + shift +fi + +unset MILL_DOWNLOAD_PATH +unset MILL_OLD_DOWNLOAD_PATH +unset OLD_MILL +unset MILL_VERSION +unset MILL_REPO_URL + +# -D mill.main.cli is for compatibility with Mill 0.10.9 - 0.13.0-M2 +# We don't quote MILL_FIRST_ARG on purpose, so we can expand the empty value without quotes +# shellcheck disable=SC2086 +exec "${MILL}" $MILL_FIRST_ARG -D "mill.main.cli=${MILL_MAIN_CLI}" "$@" diff --git a/vecxt/package.mill b/vecxt/package.mill index 625b4a4b..671a8d4a 100644 --- a/vecxt/package.mill +++ b/vecxt/package.mill @@ -45,6 +45,7 @@ object `package` extends Module { def moduleDeps = Seq(js) def moduleKind = ModuleKind.CommonJSModule } + override def enableBsp = false } object native extends VecxtModule with build.CommonNative { override def mvnDeps = super.mvnDeps() @@ -54,5 +55,6 @@ object `package` extends Module { object test extends ScalaNativeTests, VecxtTest { override def moduleDeps = Seq(native) } + override def enableBsp = false } } \ No newline at end of file From e7160b227e695ed4d46add42c38ca258f12ed51d Mon Sep 17 00:00:00 2001 From: Simon Parten Date: Mon, 29 Dec 2025 08:54:04 +0100 Subject: [PATCH 09/23] . --- laws/package.mill | 78 ++++++++++--------- laws/src/VectorMonoid.scala | 2 +- .../src/VectorMonoidAdditiveLawsSpec.scala | 4 +- .../VectorMonoidMultiplicativeLawsSpec.scala | 9 +-- 4 files changed, 48 insertions(+), 45 deletions(-) diff --git a/laws/package.mill b/laws/package.mill index 855eec97..45b26f44 100644 --- a/laws/package.mill +++ b/laws/package.mill @@ -6,7 +6,7 @@ import com.goyeau.mill.scalafix.ScalafixModule import mill.api.Task.Simple object `package` extends Module { - + val catsVersion = "2.10.0" val disciplineVersion = "2.0.0" val scalacheckVersion = "1.17.0" @@ -24,51 +24,55 @@ object `package` extends Module { object test extends ScalaTests, TestModule.Munit { def moduleDeps = Seq(jvm) - + def mvnDeps = super.mvnDeps() ++ Seq( mvn"org.scalameta::munit::${build.V.munitVersion}", mvn"org.typelevel::cats-kernel-laws:$catsVersion", mvn"org.typelevel::discipline-munit:$disciplineVersion", mvn"org.scalacheck::scalacheck:$scalacheckVersion" ) - + override def forkArgs: Simple[Seq[String]] = super.forkArgs() ++ build.vecIncubatorFlag } } - object js extends LawsModule with build.CommonJS { - def moduleDeps = Seq(build.vecxt.js) - def moduleKind = ModuleKind.ESModule - override def enableBsp = false - - object test extends ScalaJSTests, TestModule.Munit { - def moduleDeps = Seq(js) - - def mvnDeps = super.mvnDeps() ++ Seq( - mvn"org.scalameta::munit::${build.V.munitVersion}", - mvn"org.typelevel::cats-kernel-laws:$catsVersion", - mvn"org.typelevel::discipline-munit:$disciplineVersion", - mvn"org.scalacheck::scalacheck:$scalacheckVersion" - ) - - def moduleKind = ModuleKind.CommonJSModule - } - } + // NArray abstraction is also a mess :-( - object native extends LawsModule with build.CommonNative { - def moduleDeps = Seq(build.vecxt.native) - override def enableBsp = false - override def mvnDeps = super[LawsModule].mvnDeps() ++ super[CommonNative].mvnDeps() - - object test extends ScalaNativeTests, TestModule.Munit { - def moduleDeps = Seq(native) - - def mvnDeps = super.mvnDeps() ++ Seq( - mvn"org.scalameta::munit::${build.V.munitVersion}", - mvn"org.typelevel::cats-kernel-laws:$catsVersion", - mvn"org.typelevel::discipline-munit:$disciplineVersion", - mvn"org.scalacheck::scalacheck:$scalacheckVersion" - ) - } - } + // object js extends LawsModule with build.CommonJS { + // def moduleDeps = Seq(build.vecxt.js) + // def moduleKind = ModuleKind.ESModule + // override def enableBsp = false + + // object test extends ScalaJSTests, TestModule.Munit { + // def moduleDeps = Seq(js) + + // def mvnDeps = super.mvnDeps() ++ Seq( + // mvn"org.scalameta::munit::${build.V.munitVersion}", + // mvn"org.typelevel::cats-kernel-laws:$catsVersion", + // mvn"org.typelevel::discipline-munit:$disciplineVersion", + // mvn"org.scalacheck::scalacheck:$scalacheckVersion" + // ) + + // def moduleKind = ModuleKind.CommonJSModule + // } + // } + + // I think that these are not yet published for native. + + // object native extends LawsModule with build.CommonNative { + // def moduleDeps = Seq(build.vecxt.native) + // override def enableBsp = false + // override def mvnDeps = super[LawsModule].mvnDeps() ++ super[CommonNative].mvnDeps() + + // object test extends ScalaNativeTests, TestModule.Munit { + // def moduleDeps = Seq(native) + + // def mvnDeps = super.mvnDeps() ++ Seq( + // mvn"org.scalameta::munit::${build.V.munitVersion}", + // mvn"org.typelevel::cats-kernel-laws:$catsVersion", + // mvn"org.typelevel::discipline-munit:$disciplineVersion", + // mvn"org.scalacheck::scalacheck:$scalacheckVersion" + // ) + // } + // } } diff --git a/laws/src/VectorMonoid.scala b/laws/src/VectorMonoid.scala index ac173f86..0e735c6f 100644 --- a/laws/src/VectorMonoid.scala +++ b/laws/src/VectorMonoid.scala @@ -69,7 +69,7 @@ object VectorMonoid: def empty = emptyFn def combine(x: Array[A], y: Array[A]) = - if bc then + if bc then validateDim(x) validateDim(y) end if diff --git a/laws/test/src/VectorMonoidAdditiveLawsSpec.scala b/laws/test/src/VectorMonoidAdditiveLawsSpec.scala index 025d9b23..0f3af361 100644 --- a/laws/test/src/VectorMonoidAdditiveLawsSpec.scala +++ b/laws/test/src/VectorMonoidAdditiveLawsSpec.scala @@ -68,9 +68,9 @@ class VectorMonoidLawsSpec extends DisciplineSuite: // Test various dimensions testMonoidLaws(0) - testMonoidLaws(1) + testMonoidLaws(1) testMonoidLaws(3) - + // Test three random dimensions between 5-1000 private val randomDims = Random.shuffle((5 to 1000).toList).take(3) randomDims.foreach(testMonoidLaws) diff --git a/laws/test/src/VectorMonoidMultiplicativeLawsSpec.scala b/laws/test/src/VectorMonoidMultiplicativeLawsSpec.scala index 4738c92a..4cedf45e 100644 --- a/laws/test/src/VectorMonoidMultiplicativeLawsSpec.scala +++ b/laws/test/src/VectorMonoidMultiplicativeLawsSpec.scala @@ -38,7 +38,7 @@ class VectorMultiplicativeMonoidLawsSpec extends DisciplineSuite: // Create dimension witness given testDim: LawsDimension = LawsDimension(n) - // Create VectorCommutativeMonoid for this dimension + // Create VectorCommutativeMonoid for this dimension given VectorCommutativeMonoid[Double] = vectorMultiplicationMonoid(using testDim) @@ -70,11 +70,10 @@ class VectorMultiplicativeMonoidLawsSpec extends DisciplineSuite: // Test various dimensions testMonoidLaws(0) - testMonoidLaws(1) + testMonoidLaws(1) testMonoidLaws(3) - + // Test three random dimensions between 5-1000 private val randomDims = Random.shuffle((5 to 1000).toList).take(3) randomDims.foreach(testMonoidLaws) - - +end VectorMultiplicativeMonoidLawsSpec From 5cc94144c42941430febf4e88abdefb34bed17f0 Mon Sep 17 00:00:00 2001 From: Simon Parten Date: Mon, 29 Dec 2025 21:09:11 +0100 Subject: [PATCH 10/23] . --- build.mill | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.mill b/build.mill index b9e228cc..bcd249b1 100644 --- a/build.mill +++ b/build.mill @@ -27,7 +27,7 @@ import com.goyeau.mill.scalafix.ScalafixModule import mill.api.Task.Simple // def format = mill.scalalib.scalafmt.ScalafmtModule - +// mill mill.scalalib.scalafmt/ // mill __.compiledClassesAndSemanticDbFiles object V: val spire: Dep = mvn"org.typelevel::spire::0.18.0" From b6d02ccc4ff15fdd0c3124a612f727c0b19ca111 Mon Sep 17 00:00:00 2001 From: Simon Parten Date: Mon, 29 Dec 2025 21:10:09 +0100 Subject: [PATCH 11/23] . --- .scalafmt.conf | 2 +- experiments/src/mnist.scala | 2 +- vecxtensions/src-js/mathtags.scala | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index adda802b..df8d62de 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = "3.9.9" +version = "3.10.3" project.git = true runner.dialect = scala3 rewrite.scala3.convertToNewSyntax = true diff --git a/experiments/src/mnist.scala b/experiments/src/mnist.scala index 586c6001..15d0f662 100644 --- a/experiments/src/mnist.scala +++ b/experiments/src/mnist.scala @@ -105,7 +105,7 @@ import narr.* end mnist def dataToCoords(data: Array[Double]): IndexedSeq[(x: Int, y: Int, opacity: Double)] = - for (i <- 0.until(28); j <- 0.until(28)) yield + for i <- 0.until(28); j <- 0.until(28) yield val value = data(i * 28 + j) (x = j, y = 28 - i, opacity = value) diff --git a/vecxtensions/src-js/mathtags.scala b/vecxtensions/src-js/mathtags.scala index bf9ef089..0233dfa4 100644 --- a/vecxtensions/src-js/mathtags.scala +++ b/vecxtensions/src-js/mathtags.scala @@ -16,13 +16,13 @@ object MathTagsLaminar: def printMl = mfenced( mtable( - for (i <- 0 until m.rows) - yield mtr( - for (j <- 0 until m.cols) - yield mtd( - mn(m((j, i))) - ) + for i <- 0 until m.rows + yield mtr( + for j <- 0 until m.cols + yield mtd( + mn(m((j, i))) ) + ) ) ) end extension From 75d1953bc275c393e77882d6f3e56c68f2d83236 Mon Sep 17 00:00:00 2001 From: Simon Parten Date: Tue, 30 Dec 2025 07:51:17 +0100 Subject: [PATCH 12/23] . --- .devcontainer/devcontainer.json | 6 +- .github/copilot-instructions.md | 14 +- .github/workflows/benchmark.yml | 2 +- .github/workflows/ci.yml | 20 +- .github/workflows/copilot-setup-steps.yml | 6 +- .vscode/tasks.json | 2 +- experiments/src/inv_test.scala | 2 +- justfile | 4 +- millw | 327 ---------------------- 9 files changed, 28 insertions(+), 355 deletions(-) delete mode 100755 millw diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index dfa3626f..0934c380 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -22,11 +22,11 @@ "ghcr.io/devcontainers/features/java:1": { "version": 24 }, - "ghcr.io/devcontainers/features/node:1": {}, + "ghcr.io/devcontainers/features/node:1": {}, "ghcr.io/guiyomh/features/just:0" :{} }, - // Attemps to install blas so that native works. - "postCreateCommand": "chmod +x millw & npm install && sudo apt update & wait; sudo apt-get install -y libatlas-base-dev clang & wait; ./millw __.compiledClassesAndSemanticDbFiles" + // Attemps to install blas so that native works. + "postCreateCommand": "chmod +x mill & npm install && sudo apt update & wait; sudo apt-get install -y libatlas-base-dev clang & wait; ./mill __.compiledClassesAndSemanticDbFiles" // Configure tool-specific properties. // "customizations": {}, // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 511c8ebc..3f0e2dcf 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -10,16 +10,16 @@ Answer all questions in the style of a friendly colleague that is an expert in l ## Working Effectively -Mill may be found via it's wrapper script `./millw` in the root of the repository. For example `./millw vecxt.__.compile` will compile the JVM, JS and native targets. +Mill may be found via it's wrapper script `./mill` in the root of the repository. For example `./mill vecxt.__.compile` will compile the JVM, JS and native targets. Each module contains it's own build definition in the package.mill file in it's module directory. - BUILDS: Mill cold compilation takes 2 minutes or so. Stay patient! -- Compile specific platforms (e.g. jvm) with `./millw vecxt.jvm.compile` or `./millw vecxt.js.compile` etc. -- Run all tests by following with the same pattern `./millw vecxt.__.test` -- Format code with `./millw mill.scalalib.scalafmt.ScalafmtModule/`. CI will enforce formatting, and will fail if code is not formatted. -- If you see an error like this is JS `[error] @scala.scalajs.js.annotation.internal.JSType is for compiler internal use only. Do not use it yourself.`, run `./millw clean vecxt.js._` to clear the build cache. -- To run a specific main class, use the runMain command and specify the package. `./millw experiments.runMain testCheatsheet` for example. +- Compile specific platforms (e.g. jvm) with `./mill vecxt.jvm.compile` or `./mill vecxt.js.compile` etc. +- Run all tests by following with the same pattern `./mill vecxt.__.test` +- Format code with `./mill mill.scalalib.scalafmt.ScalafmtModule/`. CI will enforce formatting, and will fail if code is not formatted. +- If you see an error like this is JS `[error] @scala.scalajs.js.annotation.internal.JSType is for compiler internal use only. Do not use it yourself.`, run `./mill clean vecxt.js._` to clear the build cache. +- To run a specific main class, use the runMain command and specify the package. `./mill experiments.runMain testCheatsheet` for example. ## Folder structure @@ -29,7 +29,7 @@ vecxt/ ├── .devcontainer/ # VS Code dev container configuration ├── .vscode/ # VS Code workspace settings ├── build.mill # Mill build configuration (main build file) -├── millw # Mill wrapper script for cross-platform builds +├── mill # Mill wrapper script for cross-platform builds ├── styleguide.md # Coding style guidelines ├── benchmarks/ # Benchmarking code - not published, may be run in CI on request ├── experiments/ # Not published, inlined experiments - use this as a sandbox diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 6c7c93c4..55b9a4fd 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -17,7 +17,7 @@ jobs: jvm: temurin@17 # Run benchmark with `go test -bench` and stores the output to a file - name: Run benchmark - run: ./millw benchmark.runJmh -jvmArgs --add-modules=jdk.incubator.vector -rf json + run: ./mill benchmark.runJmh -jvmArgs --add-modules=jdk.incubator.vector -rf json - name: Set variables run: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac80f66f..e35091f8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,7 +52,7 @@ jobs: cache: npm - name: formatCheck - run: ./millw mill.scalalib.scalafmt.ScalafmtModule/checkFormatAll __.sources + run: ./mill mill.scalalib.scalafmt.ScalafmtModule/checkFormatAll __.sources - if: matrix.project == 'js' run: npm install @@ -62,21 +62,21 @@ jobs: - name: scalaJSLink if: matrix.project == 'js' - run: ./millw vecxt.${{ matrix.project }}.fastLinkJS + run: ./mill vecxt.${{ matrix.project }}.fastLinkJS - name: nativeLink if: matrix.project == 'native' - run: ./millw vecxt.${{ matrix.project }}.test.nativeLink + run: ./mill vecxt.${{ matrix.project }}.test.nativeLink - name: Test - run: ./millw vecxt.${{ matrix.project }}.test - + run: ./mill vecxt.${{ matrix.project }}.test + - name: Laws Test if: matrix.project == 'jvm' - run: ./millw laws.${{ matrix.project }}.test - + run: ./mill laws.${{ matrix.project }}.test + - name: Doc Gen - run: ./millw site.siteGen + run: ./mill site.siteGen publish: if: github.repository == 'Quafadas/vecxt' && contains(github.ref, 'refs/tags/') @@ -94,7 +94,7 @@ jobs: apps: scala-cli - name: Publish to Maven Central - run: ./millw mill.javalib.SonatypeCentralPublishModule/ + run: ./mill mill.javalib.SonatypeCentralPublishModule/ env: MILL_PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} MILL_PGP_SECRET_BASE64: ${{ secrets.PGP_SECRET }} @@ -138,7 +138,7 @@ jobs: run: mkdir -p site/docs/_assets/benchmarks && cp benchmark_history.json site/docs/_assets/benchmarks/benchmark_history.json - name: Generate static site - run: ./millw site.siteGen + run: ./mill site.siteGen - name: Setup Pages uses: actions/configure-pages@main diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index ab96015d..0169185d 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -28,7 +28,7 @@ jobs: - name: Checkout code uses: actions/checkout@v5 - - name: Setup NodeJS v18 LTS + - name: Setup NodeJS v18 LTS uses: actions/setup-node@v3 with: node-version: 20 @@ -44,8 +44,8 @@ jobs: java-version: '21' - name: Verify Java version - run: java --version + run: java --version # This should resolve a JVM, download dependancies and leave mill ready for use. - name: setup mill - run: ./millw vecxt.__.compiledClassesAndSemanticDbFiles \ No newline at end of file + run: ./mill vecxt.__.compiledClassesAndSemanticDbFiles \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 9ef233ef..46f8cb22 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -4,7 +4,7 @@ { "label": "compiledClassesAndSemanticDbFiles", "type": "shell", - "command": "./millw __.compiledClassesAndSemanticDbFiles", + "command": "./mill __.compiledClassesAndSemanticDbFiles", "runOptions": { "runOn": "folderOpen" }, diff --git a/experiments/src/inv_test.scala b/experiments/src/inv_test.scala index 5bd55d30..835fc060 100644 --- a/experiments/src/inv_test.scala +++ b/experiments/src/inv_test.scala @@ -10,7 +10,7 @@ * In [5]: x = A_inv @ b ...: x Out[5]: array([[10.], [10.], [20.], [20.], [10.]]) */ -//./millw experiments.runMain experiments.inv_test +//./mill experiments.runMain experiments.inv_test @main def inv_test = import vecxt.all.* diff --git a/justfile b/justfile index e02d74e8..26da5fd6 100644 --- a/justfile +++ b/justfile @@ -1,5 +1,5 @@ -MILL := "./millw" +MILL := "./mill" format: {{MILL}} mill.scalalib.scalafmt.ScalafmtModule/reformatAll __.sources @@ -33,7 +33,7 @@ test: testOnly target: {{MILL}} vecxt.jvm.test.testOnly vecxt.{{target}} -console: +console: {{MILL}} -i vecxt.jvm.console setJvm: diff --git a/millw b/millw deleted file mode 100755 index 555d7fc7..00000000 --- a/millw +++ /dev/null @@ -1,327 +0,0 @@ -#!/usr/bin/env sh - -# This is a wrapper script, that automatically selects or downloads Mill from Maven Central or GitHub release pages. -# -# This script determines the Mill version to use by trying these sources -# - env-variable `MILL_VERSION` -# - local file `.mill-version` -# - local file `.config/mill-version` -# - `mill-version` from YAML fronmatter of current buildfile -# - if accessible, find the latest stable version available on Maven Central (https://repo1.maven.org/maven2) -# - env-variable `DEFAULT_MILL_VERSION` -# -# If a version has the suffix '-native' a native binary will be used. -# If a version has the suffix '-jvm' an executable jar file will be used, requiring an already installed Java runtime. -# If no such suffix is found, the script will pick a default based on version and platform. -# -# Once a version was determined, it tries to use either -# - a system-installed mill, if found and it's version matches -# - an already downloaded version under ~/.cache/mill/download -# -# If no working mill version was found on the system, -# this script downloads a binary file from Maven Central or Github Pages (this is version dependent) -# into a cache location (~/.cache/mill/download). -# -# Mill Project URL: https://github.com/com-lihaoyi/mill -# Script Version: 1.0.0-M1-49-ac90e3 -# -# If you want to improve this script, please also contribute your changes back! -# This script was generated from: dist/scripts/src/mill.sh -# -# Licensed under the Apache License, Version 2.0 - -set -e - -if [ -z "${DEFAULT_MILL_VERSION}" ] ; then - DEFAULT_MILL_VERSION=0.12.14 -fi - - -if [ -z "${GITHUB_RELEASE_CDN}" ] ; then - GITHUB_RELEASE_CDN="" -fi - - -MILL_REPO_URL="https://github.com/com-lihaoyi/mill" - -if [ -z "${CURL_CMD}" ] ; then - CURL_CMD=curl -fi - -# Explicit commandline argument takes precedence over all other methods -if [ "$1" = "--mill-version" ] ; then - echo "The --mill-version option is no longer supported." 1>&2 -fi - -MILL_BUILD_SCRIPT="" - -if [ -f "build.mill" ] ; then - MILL_BUILD_SCRIPT="build.mill" -elif [ -f "build.mill.scala" ] ; then - MILL_BUILD_SCRIPT="build.mill.scala" -elif [ -f "build.sc" ] ; then - MILL_BUILD_SCRIPT="build.sc" -fi - -# Please note, that if a MILL_VERSION is already set in the environment, -# We reuse it's value and skip searching for a value. - -# If not already set, read .mill-version file -if [ -z "${MILL_VERSION}" ] ; then - if [ -f ".mill-version" ] ; then - MILL_VERSION="$(tr '\r' '\n' < .mill-version | head -n 1 2> /dev/null)" - elif [ -f ".config/mill-version" ] ; then - MILL_VERSION="$(tr '\r' '\n' < .config/mill-version | head -n 1 2> /dev/null)" - elif [ -n "${MILL_BUILD_SCRIPT}" ] ; then - MILL_VERSION="$(cat ${MILL_BUILD_SCRIPT} | grep '//[|] *mill-version: *' | sed 's;//| *mill-version: *;;')" - fi -fi - -MILL_USER_CACHE_DIR="${XDG_CACHE_HOME:-${HOME}/.cache}/mill" - -if [ -z "${MILL_DOWNLOAD_PATH}" ] ; then - MILL_DOWNLOAD_PATH="${MILL_USER_CACHE_DIR}/download" -fi - -# If not already set, try to fetch newest from Github -if [ -z "${MILL_VERSION}" ] ; then - # TODO: try to load latest version from release page - echo "No mill version specified." 1>&2 - echo "You should provide a version via a '//| mill-version: ' comment or a '.mill-version' file." 1>&2 - - mkdir -p "${MILL_DOWNLOAD_PATH}" - LANG=C touch -d '1 hour ago' "${MILL_DOWNLOAD_PATH}/.expire_latest" 2>/dev/null || ( - # we might be on OSX or BSD which don't have -d option for touch - # but probably a -A [-][[hh]mm]SS - touch "${MILL_DOWNLOAD_PATH}/.expire_latest"; touch -A -010000 "${MILL_DOWNLOAD_PATH}/.expire_latest" - ) || ( - # in case we still failed, we retry the first touch command with the intention - # to show the (previously suppressed) error message - LANG=C touch -d '1 hour ago' "${MILL_DOWNLOAD_PATH}/.expire_latest" - ) - - # POSIX shell variant of bash's -nt operator, see https://unix.stackexchange.com/a/449744/6993 - # if [ "${MILL_DOWNLOAD_PATH}/.latest" -nt "${MILL_DOWNLOAD_PATH}/.expire_latest" ] ; then - if [ -n "$(find -L "${MILL_DOWNLOAD_PATH}/.latest" -prune -newer "${MILL_DOWNLOAD_PATH}/.expire_latest")" ]; then - # we know a current latest version - MILL_VERSION=$(head -n 1 "${MILL_DOWNLOAD_PATH}"/.latest 2> /dev/null) - fi - - if [ -z "${MILL_VERSION}" ] ; then - # we don't know a current latest version - echo "Retrieving latest mill version ..." 1>&2 - LANG=C ${CURL_CMD} -s -i -f -I ${MILL_REPO_URL}/releases/latest 2> /dev/null | grep --ignore-case Location: | sed s'/^.*tag\///' | tr -d '\r\n' > "${MILL_DOWNLOAD_PATH}/.latest" - MILL_VERSION=$(head -n 1 "${MILL_DOWNLOAD_PATH}"/.latest 2> /dev/null) - fi - - if [ -z "${MILL_VERSION}" ] ; then - # Last resort - MILL_VERSION="${DEFAULT_MILL_VERSION}" - echo "Falling back to hardcoded mill version ${MILL_VERSION}" 1>&2 - else - echo "Using mill version ${MILL_VERSION}" 1>&2 - fi -fi - -MILL_NATIVE_SUFFIX="-native" -MILL_JVM_SUFFIX="-jvm" -FULL_MILL_VERSION=$MILL_VERSION -ARTIFACT_SUFFIX="" -set_artifact_suffix(){ - if [ "$(expr substr $(uname -s) 1 5 2>/dev/null)" = "Linux" ]; then - if [ "$(uname -m)" = "aarch64" ]; then - ARTIFACT_SUFFIX="-native-linux-aarch64" - else - ARTIFACT_SUFFIX="-native-linux-amd64" - fi - elif [ "$(uname)" = "Darwin" ]; then - if [ "$(uname -m)" = "arm64" ]; then - ARTIFACT_SUFFIX="-native-mac-aarch64" - else - ARTIFACT_SUFFIX="-native-mac-amd64" - fi - else - echo "This native mill launcher supports only Linux and macOS." 1>&2 - exit 1 - fi -} - -case "$MILL_VERSION" in - *"$MILL_NATIVE_SUFFIX") - MILL_VERSION=${MILL_VERSION%"$MILL_NATIVE_SUFFIX"} - set_artifact_suffix - ;; - - *"$MILL_JVM_SUFFIX") - MILL_VERSION=${MILL_VERSION%"$MILL_JVM_SUFFIX"} - ;; - - *) - case "$MILL_VERSION" in - 0.1.*) ;; - 0.2.*) ;; - 0.3.*) ;; - 0.4.*) ;; - 0.5.*) ;; - 0.6.*) ;; - 0.7.*) ;; - 0.8.*) ;; - 0.9.*) ;; - 0.10.*) ;; - 0.11.*) ;; - 0.12.*) ;; - *) - set_artifact_suffix - esac - ;; -esac - -MILL="${MILL_DOWNLOAD_PATH}/$MILL_VERSION$ARTIFACT_SUFFIX" - -try_to_use_system_mill() { - if [ "$(uname)" != "Linux" ]; then - return 0 - fi - - MILL_IN_PATH="$(command -v mill || true)" - - if [ -z "${MILL_IN_PATH}" ]; then - return 0 - fi - - SYSTEM_MILL_FIRST_TWO_BYTES=$(head --bytes=2 "${MILL_IN_PATH}") - if [ "${SYSTEM_MILL_FIRST_TWO_BYTES}" = "#!" ]; then - # MILL_IN_PATH is (very likely) a shell script and not the mill - # executable, ignore it. - return 0 - fi - - SYSTEM_MILL_PATH=$(readlink -e "${MILL_IN_PATH}") - SYSTEM_MILL_SIZE=$(stat --format=%s "${SYSTEM_MILL_PATH}") - SYSTEM_MILL_MTIME=$(stat --format=%y "${SYSTEM_MILL_PATH}") - - if [ ! -d "${MILL_USER_CACHE_DIR}" ]; then - mkdir -p "${MILL_USER_CACHE_DIR}" - fi - - SYSTEM_MILL_INFO_FILE="${MILL_USER_CACHE_DIR}/system-mill-info" - if [ -f "${SYSTEM_MILL_INFO_FILE}" ]; then - parseSystemMillInfo() { - LINE_NUMBER="${1}" - # Select the line number of the SYSTEM_MILL_INFO_FILE, cut the - # variable definition in that line in two halves and return - # the value, and finally remove the quotes. - sed -n "${LINE_NUMBER}p" "${SYSTEM_MILL_INFO_FILE}" |\ - cut -d= -f2 |\ - sed 's/"\(.*\)"/\1/' - } - - CACHED_SYSTEM_MILL_PATH=$(parseSystemMillInfo 1) - CACHED_SYSTEM_MILL_VERSION=$(parseSystemMillInfo 2) - CACHED_SYSTEM_MILL_SIZE=$(parseSystemMillInfo 3) - CACHED_SYSTEM_MILL_MTIME=$(parseSystemMillInfo 4) - - if [ "${SYSTEM_MILL_PATH}" = "${CACHED_SYSTEM_MILL_PATH}" ] \ - && [ "${SYSTEM_MILL_SIZE}" = "${CACHED_SYSTEM_MILL_SIZE}" ] \ - && [ "${SYSTEM_MILL_MTIME}" = "${CACHED_SYSTEM_MILL_MTIME}" ]; then - if [ "${CACHED_SYSTEM_MILL_VERSION}" = "${MILL_VERSION}" ]; then - MILL="${SYSTEM_MILL_PATH}" - return 0 - else - return 0 - fi - fi - fi - - SYSTEM_MILL_VERSION=$(${SYSTEM_MILL_PATH} --version | head -n1 | sed -n 's/^Mill.*version \(.*\)/\1/p') - - cat < "${SYSTEM_MILL_INFO_FILE}" -CACHED_SYSTEM_MILL_PATH="${SYSTEM_MILL_PATH}" -CACHED_SYSTEM_MILL_VERSION="${SYSTEM_MILL_VERSION}" -CACHED_SYSTEM_MILL_SIZE="${SYSTEM_MILL_SIZE}" -CACHED_SYSTEM_MILL_MTIME="${SYSTEM_MILL_MTIME}" -EOF - - if [ "${SYSTEM_MILL_VERSION}" = "${MILL_VERSION}" ]; then - MILL="${SYSTEM_MILL_PATH}" - fi -} -try_to_use_system_mill - -# If not already downloaded, download it -if [ ! -s "${MILL}" ] || [ "$MILL_TEST_DRY_RUN_LAUNCHER_SCRIPT" = "1" ] ; then - case $MILL_VERSION in - 0.0.* | 0.1.* | 0.2.* | 0.3.* | 0.4.* ) - DOWNLOAD_SUFFIX="" - DOWNLOAD_FROM_MAVEN=0 - ;; - 0.5.* | 0.6.* | 0.7.* | 0.8.* | 0.9.* | 0.10.* | 0.11.0-M* ) - DOWNLOAD_SUFFIX="-assembly" - DOWNLOAD_FROM_MAVEN=0 - ;; - *) - DOWNLOAD_SUFFIX="-assembly" - DOWNLOAD_FROM_MAVEN=1 - ;; - esac - case $MILL_VERSION in - 0.12.0 | 0.12.1 | 0.12.2 | 0.12.3 | 0.12.4 | 0.12.5 | 0.12.6 | 0.12.7 | 0.12.8 | 0.12.9 | 0.12.10 | 0.12.11 ) - DOWNLOAD_EXT="jar" - ;; - 0.12.* ) - DOWNLOAD_EXT="exe" - ;; - 0.* ) - DOWNLOAD_EXT="jar" - ;; - *) - DOWNLOAD_EXT="exe" - ;; - esac - - DOWNLOAD_FILE=$(mktemp mill.XXXXXX) - if [ "$DOWNLOAD_FROM_MAVEN" = "1" ] ; then - DOWNLOAD_URL="https://repo1.maven.org/maven2/com/lihaoyi/mill-dist${ARTIFACT_SUFFIX}/${MILL_VERSION}/mill-dist${ARTIFACT_SUFFIX}-${MILL_VERSION}.${DOWNLOAD_EXT}" - else - MILL_VERSION_TAG=$(echo "$MILL_VERSION" | sed -E 's/([^-]+)(-M[0-9]+)?(-.*)?/\1\2/') - DOWNLOAD_URL="${GITHUB_RELEASE_CDN}${MILL_REPO_URL}/releases/download/${MILL_VERSION_TAG}/${MILL_VERSION}${DOWNLOAD_SUFFIX}" - unset MILL_VERSION_TAG - fi - - if [ "$MILL_TEST_DRY_RUN_LAUNCHER_SCRIPT" = "1" ] ; then - echo $DOWNLOAD_URL - echo $MILL - exit 0 - fi - # TODO: handle command not found - echo "Downloading mill ${MILL_VERSION} from ${DOWNLOAD_URL} ..." 1>&2 - ${CURL_CMD} -f -L -o "${DOWNLOAD_FILE}" "${DOWNLOAD_URL}" - chmod +x "${DOWNLOAD_FILE}" - mkdir -p "${MILL_DOWNLOAD_PATH}" - mv "${DOWNLOAD_FILE}" "${MILL}" - - unset DOWNLOAD_FILE - unset DOWNLOAD_SUFFIX -fi - -if [ -z "$MILL_MAIN_CLI" ] ; then - MILL_MAIN_CLI="${0}" -fi - -MILL_FIRST_ARG="" -if [ "$1" = "--bsp" ] || [ "$1" = "-i" ] || [ "$1" = "--interactive" ] || [ "$1" = "--no-server" ] || [ "$1" = "--repl" ] || [ "$1" = "--help" ] ; then - # Need to preserve the first position of those listed options - MILL_FIRST_ARG=$1 - shift -fi - -unset MILL_DOWNLOAD_PATH -unset MILL_OLD_DOWNLOAD_PATH -unset OLD_MILL -unset MILL_VERSION -unset MILL_REPO_URL - -# -D mill.main.cli is for compatibility with Mill 0.10.9 - 0.13.0-M2 -# We don't quote MILL_FIRST_ARG on purpose, so we can expand the empty value without quotes -# shellcheck disable=SC2086 -exec "${MILL}" $MILL_FIRST_ARG -D "mill.main.cli=${MILL_MAIN_CLI}" "$@" From 508542bf1878f9d8d850f40c0d0025e2ebd67070 Mon Sep 17 00:00:00 2001 From: Simon Parten Date: Tue, 30 Dec 2025 07:57:55 +0100 Subject: [PATCH 13/23] . --- build.mill | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.mill b/build.mill index bcd249b1..61894f01 100644 --- a/build.mill +++ b/build.mill @@ -1,4 +1,4 @@ -//| mill-version: 1.1.0-RC2 +//| mill-version: 1.1.0-RC3 //| mill-jvm-version: 24 //| mill-jvm-opts: [ "--add-modules", "jdk.incubator.vector"] //| mvnDeps: From f7be34b0fb5a9168b8ee38d5da1b44ebec258b97 Mon Sep 17 00:00:00 2001 From: Simon Parten Date: Tue, 30 Dec 2025 08:02:30 +0100 Subject: [PATCH 14/23] . --- .gitattributes | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..1f927c4d --- /dev/null +++ b/.gitattributes @@ -0,0 +1,25 @@ +# Set default line ending behavior to auto-normalize +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.scala text +*.sc text +*.sbt text +*.conf text +*.md text +*.yml text +*.yaml text +*.json text +*.xml text +*.properties text + +# Declare files that will always have LF line endings on checkout. +*.sh text eol=lf + +# Denote all files that are truly binary and should not be modified. +*.jar binary +*.png binary +*.jpg binary +*.gif binary +*.xlsx binary From 6c071d125a8d278b13f013cbbe3cc92f3ffad768 Mon Sep 17 00:00:00 2001 From: Simon Parten Date: Tue, 30 Dec 2025 08:09:10 +0100 Subject: [PATCH 15/23] . --- .github/workflows/ci.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e35091f8..977736d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,7 +52,15 @@ jobs: cache: npm - name: formatCheck - run: ./mill mill.scalalib.scalafmt.ScalafmtModule/checkFormatAll __.sources + run: | + echo "Before format:" + git status + ./mill mill.scalalib.scalafmt.ScalafmtModule/reformatAll __.sources + echo "After format:" + git diff --name-only + echo "Differences:" + git diff + ./mill mill.scalalib.scalafmt.ScalafmtModule/checkFormatAll __.sources - if: matrix.project == 'js' run: npm install From 71de8a1c5b615fabe60b42027adfa052042130a4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Dec 2025 07:11:07 +0000 Subject: [PATCH 16/23] Re-enable cross-platform laws tests with fixed multiplication implementation Co-authored-by: Quafadas <24899792+Quafadas@users.noreply.github.com> --- .github/workflows/ci.yml | 1 - laws/src/instances/DoubleInstances.scala | 19 +++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 977736d3..e525030e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,7 +80,6 @@ jobs: run: ./mill vecxt.${{ matrix.project }}.test - name: Laws Test - if: matrix.project == 'jvm' run: ./mill laws.${{ matrix.project }}.test - name: Doc Gen diff --git a/laws/src/instances/DoubleInstances.scala b/laws/src/instances/DoubleInstances.scala index 3fb1dd2d..d833b42f 100644 --- a/laws/src/instances/DoubleInstances.scala +++ b/laws/src/instances/DoubleInstances.scala @@ -22,6 +22,9 @@ import vecxt.BoundsCheck import vecxt.all.{given, *} object double: + // Semigroup instances for Double operations + given additionSemigroup: Semigroup[Double] = Semigroup.instance[Double](_ + _) + given multiplicationSemigroup: Semigroup[Double] = Semigroup.instance[Double](_ * _) /** VectorCommutativeMonoid for Array[Double] with element-wise addition * @@ -33,20 +36,24 @@ object double: combineFn = (x, y) => import vecxt.BoundsCheck.DoBoundsCheck.yes x + y - ) + )(using additionSemigroup, BoundsCheck.DoBoundsCheck.yes) end vectorAdditionMonoid /** VectorCommutativeMonoid for Array[Double] with element-wise multiplication * - * Uses vecxt's optimized array multiplication operator which leverages SIMD on JVM Note: Element-wise multiplication - * is commutative + * Note: Element-wise multiplication is commutative. Uses manual implementation for cross-platform compatibility. */ def vectorMultiplicationMonoid(using dim: Dimension): VectorCommutativeMonoid[Double] = VectorCommutativeMonoid.forDimension(dim)( emptyFn = Array.fill(dim.size)(1.0), combineFn = (x, y) => - import vecxt.BoundsCheck.DoBoundsCheck.yes - x * y - ) + val result = new Array[Double](x.length) + var i = 0 + while i < x.length do + result(i) = x(i) * y(i) + i += 1 + end while + result + )(using multiplicationSemigroup, BoundsCheck.DoBoundsCheck.yes) end vectorMultiplicationMonoid end double From dead605ffcb8e9031a63bf4f0501424534ab0555 Mon Sep 17 00:00:00 2001 From: Simon Parten Date: Tue, 30 Dec 2025 08:24:15 +0100 Subject: [PATCH 17/23] fmt --- vecxt/src-js-native/LongArrays.scala | 8 +++----- vecxt/src-jvm/LongArrays.scala | 24 ++++++++++++------------ vecxt/src-jvm/arrays.scala | 3 ++- vecxt/test/src-jvm/IntArrays.test.scala | 1 + vecxt/test/src-jvm/test_LongArrays.scala | 2 +- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/vecxt/src-js-native/LongArrays.scala b/vecxt/src-js-native/LongArrays.scala index 22e3f29c..2a693bf4 100644 --- a/vecxt/src-js-native/LongArrays.scala +++ b/vecxt/src-js-native/LongArrays.scala @@ -2,9 +2,7 @@ package vecxt object LongArrays: + extension (arr: Array[Long]) inline def sumSIMD: Long = ??? + end extension - extension (arr: Array[Long]) - inline def sumSIMD: Long = ??? - - -end LongArrays \ No newline at end of file +end LongArrays diff --git a/vecxt/src-jvm/LongArrays.scala b/vecxt/src-jvm/LongArrays.scala index 348bd812..e774f37a 100644 --- a/vecxt/src-jvm/LongArrays.scala +++ b/vecxt/src-jvm/LongArrays.scala @@ -7,16 +7,16 @@ object LongArrays: final val length = spl.length() extension (arr: Array[Long]) - /** - * Computes the sum of all elements in the array using SIMD (Single Instruction, Multiple Data) operations. - * - * This method leverages the Vector API to perform parallel addition operations on chunks of the array, - * improving performance for large arrays. The algorithm processes the array in two phases: - * 1. Vectorized phase: Processes elements in chunks using SIMD instructions up to the loop bound - * 2. Scalar phase: Processes any remaining elements that don't fit into a complete vector - * - * @return the sum of all elements in the array as a Long value - */ + /** Computes the sum of all elements in the array using SIMD (Single Instruction, Multiple Data) operations. + * + * This method leverages the Vector API to perform parallel addition operations on chunks of the array, improving + * performance for large arrays. The algorithm processes the array in two phases: + * 1. Vectorized phase: Processes elements in chunks using SIMD instructions up to the loop bound + * 2. Scalar phase: Processes any remaining elements that don't fit into a complete vector + * + * @return + * the sum of all elements in the array as a Long value + */ inline def sumSIMD: Long = val len = arr.length var i = 0 @@ -33,6 +33,6 @@ object LongArrays: i += 1 end while total + end extension - -end LongArrays \ No newline at end of file +end LongArrays diff --git a/vecxt/src-jvm/arrays.scala b/vecxt/src-jvm/arrays.scala index 0a730827..9cbdd115 100644 --- a/vecxt/src-jvm/arrays.scala +++ b/vecxt/src-jvm/arrays.scala @@ -353,6 +353,7 @@ object arrays: i += 1 end while temp + end minSIMD inline def maxSIMD = var i = 0 @@ -370,6 +371,7 @@ object arrays: i += 1 end while temp + end maxSIMD end extension @@ -768,7 +770,6 @@ object arrays: (μ, sumSqDiff * (1.0 / (vec.length - 1))) - end meanAndVariance inline def mean: Double = vec.sumSIMD / vec.length diff --git a/vecxt/test/src-jvm/IntArrays.test.scala b/vecxt/test/src-jvm/IntArrays.test.scala index 2c7defdd..a66556c1 100644 --- a/vecxt/test/src-jvm/IntArrays.test.scala +++ b/vecxt/test/src-jvm/IntArrays.test.scala @@ -43,3 +43,4 @@ class MinMaxSIMDSuite extends munit.FunSuite: val arr = Array.tabulate(100)(i => if i == 73 then -5 else i * 2 + 10) val result = arr.minSIMD assertEquals(result, -5) +end MinMaxSIMDSuite diff --git a/vecxt/test/src-jvm/test_LongArrays.scala b/vecxt/test/src-jvm/test_LongArrays.scala index e259614b..f9355feb 100644 --- a/vecxt/test/src-jvm/test_LongArrays.scala +++ b/vecxt/test/src-jvm/test_LongArrays.scala @@ -36,4 +36,4 @@ class LongArraysSuite extends munit.FunSuite: val arr = Array.fill(LongArrays.length)(10L) assertEquals(arr.sumSIMD, 10L * LongArrays.length) -end LongArraysSuite \ No newline at end of file +end LongArraysSuite From 25aee6376cbc8ae404cbc0d5952bb08c67edafaf Mon Sep 17 00:00:00 2001 From: Simon Parten Date: Tue, 30 Dec 2025 08:25:03 +0100 Subject: [PATCH 18/23] . --- laws/src/instances/DoubleInstances.scala | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/laws/src/instances/DoubleInstances.scala b/laws/src/instances/DoubleInstances.scala index d833b42f..0dd0265d 100644 --- a/laws/src/instances/DoubleInstances.scala +++ b/laws/src/instances/DoubleInstances.scala @@ -47,13 +47,7 @@ object double: VectorCommutativeMonoid.forDimension(dim)( emptyFn = Array.fill(dim.size)(1.0), combineFn = (x, y) => - val result = new Array[Double](x.length) - var i = 0 - while i < x.length do - result(i) = x(i) * y(i) - i += 1 - end while - result + x * y )(using multiplicationSemigroup, BoundsCheck.DoBoundsCheck.yes) end vectorMultiplicationMonoid end double From cebab35d8ffdf7c282e03094df9d5e08742a28b9 Mon Sep 17 00:00:00 2001 From: Simon Parten Date: Tue, 30 Dec 2025 08:35:00 +0100 Subject: [PATCH 19/23] . --- .github/workflows/ci.yml | 1 + laws/src/VectorCommutativeGroup.scala | 72 +++++++++++++++++++ laws/src/instances/DoubleInstances.scala | 30 ++++---- .../src/VectorMonoidAdditiveLawsSpec.scala | 34 ++++----- 4 files changed, 106 insertions(+), 31 deletions(-) create mode 100644 laws/src/VectorCommutativeGroup.scala diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e525030e..977736d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,6 +80,7 @@ jobs: run: ./mill vecxt.${{ matrix.project }}.test - name: Laws Test + if: matrix.project == 'jvm' run: ./mill laws.${{ matrix.project }}.test - name: Doc Gen diff --git a/laws/src/VectorCommutativeGroup.scala b/laws/src/VectorCommutativeGroup.scala new file mode 100644 index 00000000..f6e3f7d2 --- /dev/null +++ b/laws/src/VectorCommutativeGroup.scala @@ -0,0 +1,72 @@ +/* + * Copyright 2023 quafadas + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package vecxt.laws + +import cats.kernel.{CommutativeGroup, Semigroup} +import vecxt.BoundsCheck + +/** A CommutativeGroup for Array[A] scoped to a specific dimension. + * + * This trait extends both VectorMonoid and cats.kernel.CommutativeGroup, making it compatible with cats group laws + * testing. A CommutativeGroup adds the inverse operation to a CommutativeMonoid. + * + * @tparam A + * The element type (must form a Semigroup) + */ +trait VectorCommutativeGroup[A] extends VectorMonoid[A] with CommutativeGroup[Array[A]] + +object VectorCommutativeGroup: + /** Summon a VectorCommutativeGroup instance for a specific dimension and type */ + def apply[A](using vcg: VectorCommutativeGroup[A]): VectorCommutativeGroup[A] = vcg + + /** Create a VectorCommutativeGroup instance for a specific dimension + * + * @param dim + * The dimension for this group + * @param emptyFn + * Function to create the identity element + * @param combineFn + * Function to combine two arrays (must be commutative) + * @param inverseFn + * Function to compute the inverse of an array + * @param bc + * BoundsCheck control for dimension validation + */ + def forDimension[A: Semigroup](dim: Dimension)( + emptyFn: => Array[A], + combineFn: (Array[A], Array[A]) => Array[A], + inverseFn: Array[A] => Array[A] + )(using bc: BoundsCheck.BoundsCheck = BoundsCheck.DoBoundsCheck.yes): VectorCommutativeGroup[A] = + new VectorCommutativeGroup[A]: + val dimension: Dimension = dim + + def empty = emptyFn + + def combine(x: Array[A], y: Array[A]) = + if bc == BoundsCheck.DoBoundsCheck.yes then + validateDim(x) + validateDim(y) + end if + combineFn(x, y) + end combine + + def inverse(a: Array[A]): Array[A] = + if bc == BoundsCheck.DoBoundsCheck.yes then validateDim(a) + end if + inverseFn(a) + end inverse +end VectorCommutativeGroup diff --git a/laws/src/instances/DoubleInstances.scala b/laws/src/instances/DoubleInstances.scala index 0dd0265d..4da9539c 100644 --- a/laws/src/instances/DoubleInstances.scala +++ b/laws/src/instances/DoubleInstances.scala @@ -17,37 +17,39 @@ package vecxt.laws.instances import cats.kernel.Semigroup -import vecxt.laws.{Dimension, VectorCommutativeMonoid} +import vecxt.laws.{Dimension, VectorCommutativeGroup, VectorCommutativeMonoid} import vecxt.BoundsCheck import vecxt.all.{given, *} object double: - // Semigroup instances for Double operations - given additionSemigroup: Semigroup[Double] = Semigroup.instance[Double](_ + _) - given multiplicationSemigroup: Semigroup[Double] = Semigroup.instance[Double](_ * _) - /** VectorCommutativeMonoid for Array[Double] with element-wise addition + /** VectorCommutativeGroup for Array[Double] with element-wise addition * - * Uses vecxt's optimized array addition operator which leverages SIMD on JVM + * Uses vecxt's optimized array addition operator which leverages SIMD on JVM. Addition forms a group with negation + * as the inverse operation. */ - def vectorAdditionMonoid(using dim: Dimension): VectorCommutativeMonoid[Double] = - VectorCommutativeMonoid.forDimension(dim)( + def vectorAdditionGroup(using dim: Dimension): VectorCommutativeGroup[Double] = + VectorCommutativeGroup.forDimension(dim)( emptyFn = Array.fill(dim.size)(0.0), combineFn = (x, y) => import vecxt.BoundsCheck.DoBoundsCheck.yes x + y - )(using additionSemigroup, BoundsCheck.DoBoundsCheck.yes) - end vectorAdditionMonoid + , + inverseFn = (a) => + import vecxt.BoundsCheck.DoBoundsCheck.yes + -a + ) + end vectorAdditionGroup /** VectorCommutativeMonoid for Array[Double] with element-wise multiplication * - * Note: Element-wise multiplication is commutative. Uses manual implementation for cross-platform compatibility. + * Note: Element-wise multiplication is commutative but does NOT form a group (no inverse for zero elements). Uses + * manual implementation for cross-platform compatibility. */ def vectorMultiplicationMonoid(using dim: Dimension): VectorCommutativeMonoid[Double] = VectorCommutativeMonoid.forDimension(dim)( emptyFn = Array.fill(dim.size)(1.0), - combineFn = (x, y) => - x * y - )(using multiplicationSemigroup, BoundsCheck.DoBoundsCheck.yes) + combineFn = (x, y) => x * y + ) end vectorMultiplicationMonoid end double diff --git a/laws/test/src/VectorMonoidAdditiveLawsSpec.scala b/laws/test/src/VectorMonoidAdditiveLawsSpec.scala index 0f3af361..da0cd404 100644 --- a/laws/test/src/VectorMonoidAdditiveLawsSpec.scala +++ b/laws/test/src/VectorMonoidAdditiveLawsSpec.scala @@ -16,7 +16,7 @@ package vecxt.laws -import cats.kernel.laws.discipline.{MonoidTests, CommutativeMonoidTests} +import cats.kernel.laws.discipline.CommutativeGroupTests import cats.kernel.Eq import munit.DisciplineSuite import org.scalacheck.{Arbitrary, Gen} @@ -24,21 +24,21 @@ import scala.util.Random import vecxt.laws.Dimension as LawsDimension import vecxt.laws.instances.double.* -class VectorMonoidLawsSpec extends DisciplineSuite: +class VectorGroupLawsSpec extends DisciplineSuite: // Test constants for floating-point comparisons private val MinTestValue = -100.0 private val MaxTestValue = 100.0 private val FloatingPointTolerance = 1e-10 - /** Test Monoid laws for a specific dimension */ - def testMonoidLaws(n: Int): Unit = + /** Test Group laws for a specific dimension */ + def testGroupLaws(n: Int): Unit = // Create dimension witness given testDim: LawsDimension = LawsDimension(n) - // Create VectorCommutativeMonoid for this dimension - given VectorCommutativeMonoid[Double] = - vectorAdditionMonoid(using testDim) + // Create VectorCommutativeGroup for this dimension + given VectorCommutativeGroup[Double] = + vectorAdditionGroup(using testDim) // Arbitrary generator for arrays of this dimension // Use bounded values to avoid floating point precision issues @@ -59,20 +59,20 @@ class VectorMonoidLawsSpec extends DisciplineSuite: equal ) - // Test CommutativeMonoid laws (includes all Monoid laws plus commutativity) + // Test CommutativeGroup laws (includes all Monoid laws plus commutativity and inverse) checkAll( - s"VectorCommutativeMonoid[dim$n, Double].addition", - CommutativeMonoidTests[Array[Double]].commutativeMonoid + s"VectorCommutativeGroup[dim$n, Double].addition", + CommutativeGroupTests[Array[Double]].commutativeGroup ) - end testMonoidLaws + end testGroupLaws // Test various dimensions - testMonoidLaws(0) - testMonoidLaws(1) - testMonoidLaws(3) + testGroupLaws(0) + testGroupLaws(1) + testGroupLaws(3) // Test three random dimensions between 5-1000 - private val randomDims = Random.shuffle((5 to 1000).toList).take(3) - randomDims.foreach(testMonoidLaws) + private val randomDims = Random.shuffle((5 to 1000).toList).take(2) + randomDims.foreach(testGroupLaws) -end VectorMonoidLawsSpec +end VectorGroupLawsSpec From 322675cd0c3512144c428893c8b05ee0ed194da5 Mon Sep 17 00:00:00 2001 From: Simon Parten Date: Tue, 30 Dec 2025 08:38:10 +0100 Subject: [PATCH 20/23] . --- .github/workflows/copilot-setup-steps.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 0169185d..803ef83c 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -5,6 +5,8 @@ name: "Copilot Setup Steps" on: workflow_dispatch: push: + branches: + - main paths: - .github/workflows/copilot-setup-steps.yml pull_request: From 9ee6284f6363ef12e0667953281eeacc39bf7ac3 Mon Sep 17 00:00:00 2001 From: Simon Parten Date: Wed, 31 Dec 2025 09:14:55 +0100 Subject: [PATCH 21/23] . --- benchmark/package.mill | 73 ++++++++++++++++---------------- benchmark_vs_breeze/package.mill | 23 +++++----- experiments/package.mill | 2 - jsSite/package.mill | 9 ++-- laws/package.mill | 16 +++---- site/package.mill | 10 ++--- vecxt/package.mill | 48 ++++++++++----------- vecxtensions/package.mill | 41 +++++++++--------- 8 files changed, 105 insertions(+), 117 deletions(-) diff --git a/benchmark/package.mill b/benchmark/package.mill index 96004ea0..7e5fc0ab 100644 --- a/benchmark/package.mill +++ b/benchmark/package.mill @@ -1,43 +1,42 @@ package build.benchmark -import mill._, scalalib._, publish._ +import mill.*, scalalib.*, publish.* import contrib.jmh.JmhModule // mill benchmark.runJmh vecxt.benchmark.AndBooleanBenchmark -jvmArgs --add-modules=jdk.incubator.vector -rf json -object `package` extends JmhModule with ScalaModule { - def scalaVersion = build.vecxt.jvm.scalaVersion - def jmhCoreVersion = "1.37" - override def forkArgs: T[Seq[String]] = super.forkArgs() ++ build.vecIncubatorFlag - override def moduleDeps: Seq[JavaModule] = Seq(build.vecxt.jvm) - def enableBsp = false - - // override def generateBenchmarkSources = T{ - // val dest = T.ctx().dest - - // val forkedArgs = forkArgs().toSeq - - // val sourcesDir = dest / "jmh_sources" - // val resourcesDir = dest / "jmh_resources" - - // os.remove.all(sourcesDir) - // os.makeDir.all(sourcesDir) - // os.remove.all(resourcesDir) - // os.makeDir.all(resourcesDir) - - // Jvm.runSubprocess( - // "org.openjdk.jmh.generators.bytecode.JmhBytecodeGenerator", - // (runClasspath() ++ generatorDeps()).map(_.path), - // mainArgs = Seq( - // compile().classes.path.toString, - // sourcesDir.toString, - // resourcesDir.toString, - // "default" - // ), - // jvmArgs = forkedArgs - // ) - - - // (sourcesDir, resourcesDir) - // } -} \ No newline at end of file +object `package` extends JmhModule with ScalaModule: + def scalaVersion = build.vecxt.jvm.scalaVersion + def jmhCoreVersion = "1.37" + override def forkArgs: T[Seq[String]] = super.forkArgs() ++ build.vecIncubatorFlag + override def moduleDeps: Seq[JavaModule] = Seq(build.vecxt.jvm) + def enableBsp = false + + // override def generateBenchmarkSources = T{ + // val dest = T.ctx().dest + + // val forkedArgs = forkArgs().toSeq + + // val sourcesDir = dest / "jmh_sources" + // val resourcesDir = dest / "jmh_resources" + + // os.remove.all(sourcesDir) + // os.makeDir.all(sourcesDir) + // os.remove.all(resourcesDir) + // os.makeDir.all(resourcesDir) + + // Jvm.runSubprocess( + // "org.openjdk.jmh.generators.bytecode.JmhBytecodeGenerator", + // (runClasspath() ++ generatorDeps()).map(_.path), + // mainArgs = Seq( + // compile().classes.path.toString, + // sourcesDir.toString, + // resourcesDir.toString, + // "default" + // ), + // jvmArgs = forkedArgs + // ) + + // (sourcesDir, resourcesDir) + // } +end `package` diff --git a/benchmark_vs_breeze/package.mill b/benchmark_vs_breeze/package.mill index 26652f2c..9531fdcb 100644 --- a/benchmark_vs_breeze/package.mill +++ b/benchmark_vs_breeze/package.mill @@ -1,18 +1,17 @@ package build.benchmark_vs_breeze -import mill._, scalalib._, publish._ +import mill.*, scalalib.*, publish.* import contrib.jmh.JmhModule // mill benchmark.runJmh vecxt.benchmark.AndBooleanBenchmark -jvmArgs --add-modules=jdk.incubator.vector -rf json -object `package` extends JmhModule with ScalaModule { - def scalaVersion = build.vecxt.jvm.scalaVersion - def jmhCoreVersion = "1.37" - def enableBsp = false - override def forkArgs: T[Seq[String]] = super.forkArgs() ++ build.vecIncubatorFlag - override def moduleDeps: Seq[JavaModule] = Seq(build.vecxt.jvm) - override def mvnDeps = super.mvnDeps() ++ Seq( - mvn"org.scalanlp::breeze:2.1.0" - ) - -} \ No newline at end of file +object `package` extends JmhModule with ScalaModule: + def scalaVersion = build.vecxt.jvm.scalaVersion + def jmhCoreVersion = "1.37" + def enableBsp = false + override def forkArgs: T[Seq[String]] = super.forkArgs() ++ build.vecIncubatorFlag + override def moduleDeps: Seq[JavaModule] = Seq(build.vecxt.jvm) + override def mvnDeps = super.mvnDeps() ++ Seq( + mvn"org.scalanlp::breeze:2.1.0" + ) +end `package` diff --git a/experiments/package.mill b/experiments/package.mill index c54185d7..48f22f0d 100644 --- a/experiments/package.mill +++ b/experiments/package.mill @@ -15,8 +15,6 @@ object `package` extends ScalaModule: override def forkArgs = super.forkArgs() ++ build.vecIncubatorFlag // override def mainClass = Some("mnist") - - override def moduleDeps = Seq(build.vecxt.jvm) override def mvnDeps = super.mvnDeps() ++ Seq( mvn"com.lihaoyi::os-lib::0.10.4", diff --git a/jsSite/package.mill b/jsSite/package.mill index 5c0f774a..88049601 100644 --- a/jsSite/package.mill +++ b/jsSite/package.mill @@ -7,16 +7,15 @@ import mill.util.VcsVersion import build.V import mill.scalajslib.api.ESModuleImportMapping - -object `package` extends SiteJSModule { +object `package` extends SiteJSModule: override def moduleDeps = Seq(build.vecxt.js, build.vecxtensions.js) override def scalaVersion = build.vecxt.js.scalaVersion override def scalaJSVersion = build.vecxt.js.scalaJSVersion - override def scalaJSImportMap = Task{ + override def scalaJSImportMap = Task { Seq( - ESModuleImportMapping.Prefix("@stdlib/blas/base", "https://cdn.jsdelivr.net/npm/@stdlib/blas@0.2.0/base/+esm" ) + ESModuleImportMapping.Prefix("@stdlib/blas/base", "https://cdn.jsdelivr.net/npm/@stdlib/blas@0.2.0/base/+esm") ) } @@ -35,4 +34,4 @@ object `package` extends SiteJSModule { // object test extends ScalaJSTests with CommonTests { // def moduleKind = ModuleKind.CommonJSModule // } -} \ No newline at end of file +end `package` diff --git a/laws/package.mill b/laws/package.mill index 45b26f44..63298887 100644 --- a/laws/package.mill +++ b/laws/package.mill @@ -5,24 +5,24 @@ import mill.scalajslib.api.ModuleKind import com.goyeau.mill.scalafix.ScalafixModule import mill.api.Task.Simple -object `package` extends Module { +object `package` extends Module: val catsVersion = "2.10.0" val disciplineVersion = "2.0.0" val scalacheckVersion = "1.17.0" - trait LawsModule extends PlatformScalaModule with build.VecxtPublishModule { + trait LawsModule extends PlatformScalaModule with build.VecxtPublishModule: def mvnDeps = super.mvnDeps() ++ Seq( mvn"org.typelevel::cats-kernel:$catsVersion" ) - } + end LawsModule - object jvm extends LawsModule { + object jvm extends LawsModule: def moduleDeps = Seq(build.vecxt.jvm) override def scalaVersion = build.V.scalaVersion override def forkArgs = super.forkArgs() ++ build.vecIncubatorFlag - object test extends ScalaTests, TestModule.Munit { + object test extends ScalaTests, TestModule.Munit: def moduleDeps = Seq(jvm) def mvnDeps = super.mvnDeps() ++ Seq( @@ -33,8 +33,8 @@ object `package` extends Module { ) override def forkArgs: Simple[Seq[String]] = super.forkArgs() ++ build.vecIncubatorFlag - } - } + end test + end jvm // NArray abstraction is also a mess :-( @@ -75,4 +75,4 @@ object `package` extends Module { // ) // } // } -} +end `package` diff --git a/site/package.mill b/site/package.mill index 31cd136b..462e1223 100644 --- a/site/package.mill +++ b/site/package.mill @@ -5,8 +5,7 @@ import mill.scalajslib.api.* import io.github.quafadas.millSite.* import mill.util.VcsVersion - -object `package` extends SiteModule { +object `package` extends SiteModule: override lazy val jsSiteModule = Some(build.jsSite) // override def pathToImportMap = Some(PathRef(T.workspace / "importmap.json")) @@ -20,9 +19,9 @@ object `package` extends SiteModule { override def unidocDeps = Seq(build.vecxt.jvm) - override def scalaJSImportMap = Task{ + override def scalaJSImportMap = Task { Seq( - ESModuleImportMapping.Prefix("@stdlib/blas/base", "https://cdn.jsdelivr.net/npm/@stdlib/blas@0.2.0/base/+esm" ) + ESModuleImportMapping.Prefix("@stdlib/blas/base", "https://cdn.jsdelivr.net/npm/@stdlib/blas@0.2.0/base/+esm") ) } @@ -33,5 +32,4 @@ object `package` extends SiteModule { // logLevel = "debug" // ) // } - -} \ No newline at end of file +end `package` diff --git a/vecxt/package.mill b/vecxt/package.mill index 671a8d4a..1c62d414 100644 --- a/vecxt/package.mill +++ b/vecxt/package.mill @@ -5,56 +5,54 @@ import mill.scalajslib.api.ModuleKind import com.goyeau.mill.scalafix.ScalafixModule import mill.api.Task.Simple - -object `package` extends Module { - trait VecxtModule extends PlatformScalaModule with build.VecxtPublishModule { +object `package` extends Module: + trait VecxtModule extends PlatformScalaModule with build.VecxtPublishModule: def mvnDeps = super.mvnDeps() ++ Seq( build.V.narr ) - trait VecxtTest extends ScalaTests, TestModule.Munit { + trait VecxtTest extends ScalaTests, TestModule.Munit: def mvnDeps = super.mvnDeps() ++ Seq( - mvn"org.scalameta::munit::${build.V.munitVersion}", + mvn"org.scalameta::munit::${build.V.munitVersion}" ) override def forkArgs: Simple[Seq[String]] = super.forkArgs() ++ build.vecIncubatorFlag - } - } + end VecxtTest + end VecxtModule - def jsNativeSharedSources = Task.Sources{ + def jsNativeSharedSources = Task.Sources { (os.sub / "src-js-native") } - - object jvm extends VecxtModule { + object jvm extends VecxtModule: override def scalaVersion = build.V.scalaVersion override def forkArgs = super.forkArgs() ++ build.vecIncubatorFlag def mvnDeps = super.mvnDeps() ++ Seq( build.V.blas, - build.V.lapack, + build.V.lapack ) - object test extends VecxtTest, ScalaTests { + object test extends VecxtTest, ScalaTests: def moduleDeps = Seq(jvm) override def forkArgs: Simple[Seq[String]] = super.forkArgs() ++ build.vecIncubatorFlag - } - } - object js extends VecxtModule with build.CommonJS { + end test + end jvm + object js extends VecxtModule with build.CommonJS: override def mvnDeps = super.mvnDeps() def moduleKind = ModuleKind.ESModule - def sources = Task{super.sources() ++ jsNativeSharedSources()} - object test extends VecxtTest, ScalaJSTests { + def sources = Task(super.sources() ++ jsNativeSharedSources()) + object test extends VecxtTest, ScalaJSTests: def moduleDeps = Seq(js) def moduleKind = ModuleKind.CommonJSModule - } + end test override def enableBsp = false - } - object native extends VecxtModule with build.CommonNative { + end js + object native extends VecxtModule with build.CommonNative: override def mvnDeps = super.mvnDeps() - def sources = Task{super.sources() ++ jsNativeSharedSources()} + def sources = Task(super.sources() ++ jsNativeSharedSources()) - object test extends ScalaNativeTests, VecxtTest { + object test extends ScalaNativeTests, VecxtTest: override def moduleDeps = Seq(native) - } + end test override def enableBsp = false - } -} \ No newline at end of file + end native +end `package` diff --git a/vecxtensions/package.mill b/vecxtensions/package.mill index 3bf95a17..ec489bcf 100644 --- a/vecxtensions/package.mill +++ b/vecxtensions/package.mill @@ -5,24 +5,21 @@ import mill.scalajslib.api.ModuleKind import com.goyeau.mill.scalafix.ScalafixModule import mill.api.Task.Simple - -object `package` extends Module { - trait VecxtensionModule extends PlatformScalaModule with build.VecxtPublishModule { +object `package` extends Module: + trait VecxtensionModule extends PlatformScalaModule with build.VecxtPublishModule: def mvnDeps = super.mvnDeps() ++ Seq( build.V.narr, build.V.scalaTags ) - trait VecxtTest extends ScalaTests, TestModule.Munit { + trait VecxtTest extends ScalaTests, TestModule.Munit: def mvnDeps = super.mvnDeps() ++ Seq( - mvn"org.scalameta::munit::${build.V.munitVersion}", + mvn"org.scalameta::munit::${build.V.munitVersion}" ) override def forkArgs: Simple[Seq[String]] = super.forkArgs() ++ build.vecIncubatorFlag - } - } - - + end VecxtTest + end VecxtensionModule - object jvm extends VecxtensionModule { + object jvm extends VecxtensionModule: def moduleDeps = Seq(build.vecxt.jvm) override def scalaVersion = build.V.scalaVersion override def forkArgs = super.forkArgs() ++ build.vecIncubatorFlag @@ -31,12 +28,12 @@ object `package` extends Module { build.V.spire ) - object test extends VecxtTest, ScalaTests { + object test extends VecxtTest, ScalaTests: def moduleDeps = Seq(jvm) override def forkArgs: Simple[Seq[String]] = super.forkArgs() ++ build.vecIncubatorFlag - } - } - object js extends VecxtensionModule with build.CommonJS { + end test + end jvm + object js extends VecxtensionModule with build.CommonJS: def moduleDeps = Seq(build.vecxt.js) override def mvnDeps = super.mvnDeps() ++ Seq( build.V.laminar, @@ -44,18 +41,18 @@ object `package` extends Module { ) def enableBsp = false def moduleKind = ModuleKind.ESModule - object test extends VecxtTest, ScalaJSTests { + object test extends VecxtTest, ScalaJSTests: def moduleDeps = Seq(js) def moduleKind = ModuleKind.CommonJSModule - } - } - object native extends VecxtensionModule with build.CommonNative { + end test + end js + object native extends VecxtensionModule with build.CommonNative: def moduleDeps = Seq(build.vecxt.native) override def mvnDeps = super.mvnDeps() def enableBsp = false - object test extends ScalaNativeTests, VecxtTest { + object test extends ScalaNativeTests, VecxtTest: override def moduleDeps = Seq(native) - } - } -} \ No newline at end of file + end test + end native +end `package` From 15a7ca8d27a5d4ca9c42c03afc503ca34dd7d403 Mon Sep 17 00:00:00 2001 From: Simon Parten Date: Fri, 2 Jan 2026 22:26:43 +0100 Subject: [PATCH 22/23] . --- .github/workflows/ci.yml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 977736d3..e35091f8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,15 +52,7 @@ jobs: cache: npm - name: formatCheck - run: | - echo "Before format:" - git status - ./mill mill.scalalib.scalafmt.ScalafmtModule/reformatAll __.sources - echo "After format:" - git diff --name-only - echo "Differences:" - git diff - ./mill mill.scalalib.scalafmt.ScalafmtModule/checkFormatAll __.sources + run: ./mill mill.scalalib.scalafmt.ScalafmtModule/checkFormatAll __.sources - if: matrix.project == 'js' run: npm install From 475f756ad32d4fe9da9733ba73a6d0eb4d79a11c Mon Sep 17 00:00:00 2001 From: Simon Parten Date: Thu, 8 Jan 2026 14:34:35 +0100 Subject: [PATCH 23/23] . --- .github/workflows/ci.yml | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e35091f8..1263f41e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,4 @@ -# This file was automatically generated by sbt-github-actions using the -# githubWorkflowGenerate task. You should add and commit this file to -# your git repository. It goes without saying that you shouldn't edit -# this file by hand! Instead, if you wish to make changes, you should -# change your sbt build configuration to revise the workflow description -# to meet your needs, then regenerate this file. - name: Continuous Integration - on: pull_request: branches: ['**', '!update/**', '!pr/**'] @@ -16,6 +8,7 @@ on: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + FORCE_COLOR: '1' concurrency: @@ -76,8 +69,20 @@ jobs: run: ./mill laws.${{ matrix.project }}.test - name: Doc Gen + if: matrix.project == 'jvm' run: ./mill site.siteGen + - name: Publish Test Report + uses: mikepenz/action-junit-report@v5 + if: always() + with: + fail_on_failure: false + include_passed: false + detailed_summary: true + annotate_only: true + require_tests: false + report_paths: 'out/**/test-report.xml' + publish: if: github.repository == 'Quafadas/vecxt' && contains(github.ref, 'refs/tags/') needs: build