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/.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 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 0d5e74d0..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: @@ -52,7 +45,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,16 +55,33 @@ 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: ./mill laws.${{ matrix.project }}.test + - name: Doc Gen - run: ./millw site.siteGen + 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/') @@ -89,7 +99,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 }} @@ -133,7 +143,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..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: @@ -28,7 +30,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 +46,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/.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/.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/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/build.mill b/build.mill index b9e228cc..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: @@ -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" 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/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/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/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/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/laws/package.mill b/laws/package.mill new file mode 100644 index 00000000..63298887 --- /dev/null +++ b/laws/package.mill @@ -0,0 +1,78 @@ +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" + ) + end 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: + 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 + end test + end jvm + + // NArray abstraction is also a mess :-( + + // 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" + // ) + // } + // } +end `package` diff --git a/laws/src/Dimension.scala b/laws/src/Dimension.scala new file mode 100644 index 00000000..2a3b2dfa --- /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 extension +end Dimension 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/VectorCommutativeMonoid.scala b/laws/src/VectorCommutativeMonoid.scala new file mode 100644 index 00000000..96386b86 --- /dev/null +++ b/laws/src/VectorCommutativeMonoid.scala @@ -0,0 +1,63 @@ +/* + * 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) + end if + combineFn(x, y) + end combine +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..0e735c6f --- /dev/null +++ b/laws/src/VectorMonoid.scala @@ -0,0 +1,78 @@ +/* + * 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): VectorMonoid[A] = + new VectorMonoid[A]: + val dimension: Dimension = dim + + def empty = emptyFn + + def combine(x: Array[A], y: Array[A]) = + if bc 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 new file mode 100644 index 00000000..4da9539c --- /dev/null +++ b/laws/src/instances/DoubleInstances.scala @@ -0,0 +1,55 @@ +/* + * 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, VectorCommutativeGroup, VectorCommutativeMonoid} +import vecxt.BoundsCheck +import vecxt.all.{given, *} + +object double: + + /** VectorCommutativeGroup for Array[Double] with element-wise addition + * + * Uses vecxt's optimized array addition operator which leverages SIMD on JVM. Addition forms a group with negation + * as the inverse operation. + */ + 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 + , + inverseFn = (a) => + import vecxt.BoundsCheck.DoBoundsCheck.yes + -a + ) + end vectorAdditionGroup + + /** VectorCommutativeMonoid for Array[Double] with element-wise multiplication + * + * 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 + ) + end vectorMultiplicationMonoid +end double diff --git a/laws/test/src/VectorMonoidAdditiveLawsSpec.scala b/laws/test/src/VectorMonoidAdditiveLawsSpec.scala new file mode 100644 index 00000000..da0cd404 --- /dev/null +++ b/laws/test/src/VectorMonoidAdditiveLawsSpec.scala @@ -0,0 +1,78 @@ +/* + * 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.CommutativeGroupTests +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 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 Group laws for a specific dimension */ + def testGroupLaws(n: Int): Unit = + // Create dimension witness + given testDim: LawsDimension = LawsDimension(n) + + // 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 + 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 CommutativeGroup laws (includes all Monoid laws plus commutativity and inverse) + checkAll( + s"VectorCommutativeGroup[dim$n, Double].addition", + CommutativeGroupTests[Array[Double]].commutativeGroup + ) + end testGroupLaws + + // Test various dimensions + testGroupLaws(0) + testGroupLaws(1) + testGroupLaws(3) + + // Test three random dimensions between 5-1000 + private val randomDims = Random.shuffle((5 to 1000).toList).take(2) + randomDims.foreach(testGroupLaws) + +end VectorGroupLawsSpec diff --git a/laws/test/src/VectorMonoidMultiplicativeLawsSpec.scala b/laws/test/src/VectorMonoidMultiplicativeLawsSpec.scala new file mode 100644 index 00000000..4cedf45e --- /dev/null +++ b/laws/test/src/VectorMonoidMultiplicativeLawsSpec.scala @@ -0,0 +1,79 @@ +/* + * 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) +end VectorMultiplicativeMonoidLawsSpec diff --git a/millw b/mill similarity index 100% rename from millw rename to mill 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) 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 625b4a4b..1c62d414 100644 --- a/vecxt/package.mill +++ b/vecxt/package.mill @@ -5,54 +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 - } - } - object native extends VecxtModule with build.CommonNative { + end test + override def enableBsp = false + 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) - } - } -} \ No newline at end of file + end test + override def enableBsp = false + end native +end `package` 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 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` 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