From 22a54c1df7f771d390e53c2dd32a8f00078dccde Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 14 Jan 2023 08:24:14 -0700 Subject: [PATCH 001/141] fix merge conflict --- .github/workflows/ci.yml | 4 ++-- build.sbt | 12 +++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99d3ce0e9..5ff1206f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,11 +80,11 @@ jobs: - name: Make target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/scala3') - run: mkdir -p spire/.js/target spire/.jvm/target testkit/.native/target units/.jvm/target testkit/.js/target unidocs/target core/.native/target spire/.native/target core/.js/target units/.native/target core/.jvm/target refined/.native/target refined/.js/target refined/.jvm/target units/.js/target testkit/.jvm/target project/target + run: mkdir -p spire/.js/target spire/.jvm/target testkit/.native/target units/.jvm/target testkit/.js/target parser/.jvm/target unidocs/target parser/.js/target core/.native/target spire/.native/target parser/.native/target core/.js/target units/.native/target core/.jvm/target refined/.native/target refined/.js/target refined/.jvm/target units/.js/target testkit/.jvm/target project/target - name: Compress target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/scala3') - run: tar cf targets.tar spire/.js/target spire/.jvm/target testkit/.native/target units/.jvm/target testkit/.js/target unidocs/target core/.native/target spire/.native/target core/.js/target units/.native/target core/.jvm/target refined/.native/target refined/.js/target refined/.jvm/target units/.js/target testkit/.jvm/target project/target + run: tar cf targets.tar spire/.js/target spire/.jvm/target testkit/.native/target units/.jvm/target testkit/.js/target parser/.jvm/target unidocs/target parser/.js/target core/.native/target spire/.native/target parser/.native/target core/.js/target units/.native/target core/.jvm/target refined/.native/target refined/.js/target refined/.jvm/target units/.js/target testkit/.jvm/target project/target - name: Upload target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/scala3') diff --git a/build.sbt b/build.sbt index 54e75f8f9..da95da7a8 100644 --- a/build.sbt +++ b/build.sbt @@ -38,7 +38,7 @@ def commonSettings = Seq( ) lazy val root = tlCrossRootProject - .aggregate(core, units, spire, refined, testkit, unidocs) + .aggregate(core, units, parser, spire, refined, testkit, unidocs) lazy val core = crossProject(JVMPlatform, JSPlatform, NativePlatform) .crossType(CrossType.Pure) @@ -60,6 +60,15 @@ lazy val units = crossProject(JVMPlatform, JSPlatform, NativePlatform) libraryDependencies += "io.github.cquiroz" %%% "scala-java-time" % "2.5.0" % Test ) +// see also: https://github.com/lampepfl/dotty/issues/7647 +lazy val parser = crossProject(JVMPlatform, JSPlatform, NativePlatform) + .crossType(CrossType.Pure) + .in(file("parser")) + .settings(name := "coulomb-parser") + .dependsOn(core % "compile->compile;test->test") + .settings(commonSettings: _*) + .settings(libraryDependencies += "org.scala-lang" %% "scala3-staging" % scalaVersion.value) + lazy val spire = crossProject(JVMPlatform, JSPlatform, NativePlatform) .crossType(CrossType.Pure) .in(file("spire")) @@ -102,6 +111,7 @@ lazy val all = project .dependsOn( core.jvm, units.jvm, + parser.jvm, spire.jvm, refined.jvm ) // scala repl only needs JVMPlatform subproj builds From bf8640879a1068df537152794f1b8abf40eeee35 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 14 Jan 2023 08:32:11 -0700 Subject: [PATCH 002/141] reformat build.sbt --- build.sbt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index da95da7a8..d00ec251e 100644 --- a/build.sbt +++ b/build.sbt @@ -67,7 +67,9 @@ lazy val parser = crossProject(JVMPlatform, JSPlatform, NativePlatform) .settings(name := "coulomb-parser") .dependsOn(core % "compile->compile;test->test") .settings(commonSettings: _*) - .settings(libraryDependencies += "org.scala-lang" %% "scala3-staging" % scalaVersion.value) + .settings( + libraryDependencies += "org.scala-lang" %% "scala3-staging" % scalaVersion.value + ) lazy val spire = crossProject(JVMPlatform, JSPlatform, NativePlatform) .crossType(CrossType.Pure) From 207510ccfa40f4c16f0f461f93c30a644080b3ba Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 14 Jan 2023 08:45:29 -0700 Subject: [PATCH 003/141] parser.scala --- .../main/scala/coulomb/parser/parser.scala | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 parser/src/main/scala/coulomb/parser/parser.scala diff --git a/parser/src/main/scala/coulomb/parser/parser.scala b/parser/src/main/scala/coulomb/parser/parser.scala new file mode 100644 index 000000000..253f61929 --- /dev/null +++ b/parser/src/main/scala/coulomb/parser/parser.scala @@ -0,0 +1,20 @@ +/* + * Copyright 2022 Erik Erlandson + * + * 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 coulomb.parser + +object test: + val stub = 0 From 25a4a22e6422d8609796a7f3d98adbfde7ee4a3e Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 14 Jan 2023 08:58:32 -0700 Subject: [PATCH 004/141] toVersionIntroduced --- build.sbt | 1 + 1 file changed, 1 insertion(+) diff --git a/build.sbt b/build.sbt index d00ec251e..6e932312e 100644 --- a/build.sbt +++ b/build.sbt @@ -68,6 +68,7 @@ lazy val parser = crossProject(JVMPlatform, JSPlatform, NativePlatform) .dependsOn(core % "compile->compile;test->test") .settings(commonSettings: _*) .settings( + tlVersionIntroduced := Map("3" -> "0.7.3"), libraryDependencies += "org.scala-lang" %% "scala3-staging" % scalaVersion.value ) From b27d99159f601675398b8474f5dcba07e0b214e0 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 14 Jan 2023 10:08:09 -0700 Subject: [PATCH 005/141] test f --- parser/src/main/scala/coulomb/parser/parser.scala | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/parser/src/main/scala/coulomb/parser/parser.scala b/parser/src/main/scala/coulomb/parser/parser.scala index 253f61929..4e7ba7326 100644 --- a/parser/src/main/scala/coulomb/parser/parser.scala +++ b/parser/src/main/scala/coulomb/parser/parser.scala @@ -18,3 +18,12 @@ package coulomb.parser object test: val stub = 0 + + inline def f[T]: String = ${ meta.f[T] } + + object meta: + import scala.quoted.* + import scala.language.implicitConversions + + def f[T](using Quotes, Type[T]): Expr[String] = + Expr("foo!") From 99133c81ea9ee9d7e9dfb759d8737395f2afe102 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 14 Jan 2023 11:32:04 -0700 Subject: [PATCH 006/141] reformat --- parser/src/main/scala/coulomb/parser/parser.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parser/src/main/scala/coulomb/parser/parser.scala b/parser/src/main/scala/coulomb/parser/parser.scala index 4e7ba7326..c75c8a40b 100644 --- a/parser/src/main/scala/coulomb/parser/parser.scala +++ b/parser/src/main/scala/coulomb/parser/parser.scala @@ -24,6 +24,6 @@ object test: object meta: import scala.quoted.* import scala.language.implicitConversions - + def f[T](using Quotes, Type[T]): Expr[String] = Expr("foo!") From ec539047b5048b0d46c1a587180a1cffa5d0c077 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 14 Jan 2023 13:25:36 -0700 Subject: [PATCH 007/141] expr of map --- parser/src/main/scala/coulomb/parser/parser.scala | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/parser/src/main/scala/coulomb/parser/parser.scala b/parser/src/main/scala/coulomb/parser/parser.scala index c75c8a40b..bc35831f4 100644 --- a/parser/src/main/scala/coulomb/parser/parser.scala +++ b/parser/src/main/scala/coulomb/parser/parser.scala @@ -21,9 +21,15 @@ object test: inline def f[T]: String = ${ meta.f[T] } + inline def m[T]: Map[String, String] = ${ meta.m[T] } + object meta: import scala.quoted.* import scala.language.implicitConversions def f[T](using Quotes, Type[T]): Expr[String] = Expr("foo!") + + def m[T](using Quotes, Type[T]): Expr[Map[String, String]] = + val mm = Map("a" -> "b") + Expr(mm) From 08d459f6bf877ca3084cd5804d914c533e948eec Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 15 Jan 2023 10:41:34 -0700 Subject: [PATCH 008/141] test of runtime TypeRepr build (not working) --- build.sbt | 2 +- .../main/scala/coulomb/parser/parser.scala | 51 +++++++++++++++++-- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/build.sbt b/build.sbt index 6e932312e..5fa21f115 100644 --- a/build.sbt +++ b/build.sbt @@ -65,7 +65,7 @@ lazy val parser = crossProject(JVMPlatform, JSPlatform, NativePlatform) .crossType(CrossType.Pure) .in(file("parser")) .settings(name := "coulomb-parser") - .dependsOn(core % "compile->compile;test->test") + .dependsOn(core % "compile->compile;test->test", units) // make units "% Test" .settings(commonSettings: _*) .settings( tlVersionIntroduced := Map("3" -> "0.7.3"), diff --git a/parser/src/main/scala/coulomb/parser/parser.scala b/parser/src/main/scala/coulomb/parser/parser.scala index bc35831f4..7b850583d 100644 --- a/parser/src/main/scala/coulomb/parser/parser.scala +++ b/parser/src/main/scala/coulomb/parser/parser.scala @@ -16,19 +16,62 @@ package coulomb.parser +import scala.quoted.* + +import coulomb.* +import coulomb.syntax.* +import coulomb.units.si.* +import coulomb.conversion.* + object test: val stub = 0 - inline def f[T]: String = ${ meta.f[T] } + // fully qualified type name + inline def fqtn[T]: String = ${ meta.fqtn[T] } inline def m[T]: Map[String, String] = ${ meta.m[T] } + // this "almost" works but it won't bind 'Quotes' if you try to invoke it + def q(v: Double, u: String)(using + Quotes, + staging.Compiler + ): Quantity[Double, Meter] = staging.run { + import quotes.reflect.* + + // this eventually builds arbitrary unit expr via parsing + val t = u match + case "meter" => TypeRepr.of[Meter] + case "second" => TypeRepr.of[Second] + case _ => TypeRepr.of[1] + + val ttt = t.asType match + case '[t] => Expr.summon[UnitConversion[Double, t, Meter]] + val cnv = ttt match + case Some(x) => x + case _ => null + + val f = t.asType match + case '[t] => + '{ (v: Double) => + v.withUnit[t] + .toUnit[Meter](using + ${ + cnv.asTerm + .asExprOf[UnitConversion[Double, t, Meter]] + } + ) + } + + '{ (${ f })(${ Expr(v) }) } + } + object meta: - import scala.quoted.* import scala.language.implicitConversions - def f[T](using Quotes, Type[T]): Expr[String] = - Expr("foo!") + def fqtn[T](using Quotes, Type[T]): Expr[String] = + import quotes.reflect.* + val tr = TypeRepr.of[T] + Expr(tr.dealias.typeSymbol.fullName) def m[T](using Quotes, Type[T]): Expr[Map[String, String]] = val mm = Map("a" -> "b") From cec9f648570293c7cc73379ad206cf12cbd4b43c Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 15 Jan 2023 16:36:16 -0700 Subject: [PATCH 009/141] closer --- build.sbt | 5 +- .../main/scala/coulomb/parser/parser.scala | 86 +++++++++++++------ 2 files changed, 63 insertions(+), 28 deletions(-) diff --git a/build.sbt b/build.sbt index 5fa21f115..a59ae72cf 100644 --- a/build.sbt +++ b/build.sbt @@ -65,7 +65,10 @@ lazy val parser = crossProject(JVMPlatform, JSPlatform, NativePlatform) .crossType(CrossType.Pure) .in(file("parser")) .settings(name := "coulomb-parser") - .dependsOn(core % "compile->compile;test->test", units) // make units "% Test" + .dependsOn( + core % "compile->compile;test->test", + units + ) // make units "% Test" .settings(commonSettings: _*) .settings( tlVersionIntroduced := Map("3" -> "0.7.3"), diff --git a/parser/src/main/scala/coulomb/parser/parser.scala b/parser/src/main/scala/coulomb/parser/parser.scala index 7b850583d..ead06dfcc 100644 --- a/parser/src/main/scala/coulomb/parser/parser.scala +++ b/parser/src/main/scala/coulomb/parser/parser.scala @@ -31,43 +31,75 @@ object test: inline def m[T]: Map[String, String] = ${ meta.m[T] } - // this "almost" works but it won't bind 'Quotes' if you try to invoke it + // this compiles and "runs" but run-time fails trying to find + // coulomb implicits - so the staging compiler + // currently doesn't work for finding imported implicits + // unsure if this is due to bad class loader or something else def q(v: Double, u: String)(using - Quotes, staging.Compiler ): Quantity[Double, Meter] = staging.run { - import quotes.reflect.* - - // this eventually builds arbitrary unit expr via parsing - val t = u match - case "meter" => TypeRepr.of[Meter] - case "second" => TypeRepr.of[Second] - case _ => TypeRepr.of[1] - - val ttt = t.asType match - case '[t] => Expr.summon[UnitConversion[Double, t, Meter]] - val cnv = ttt match - case Some(x) => x - case _ => null - - val f = t.asType match - case '[t] => - '{ (v: Double) => - v.withUnit[t] - .toUnit[Meter](using - ${ - cnv.asTerm - .asExprOf[UnitConversion[Double, t, Meter]] - } - ) - } + // inside this scope Quotes is defined + println(s"get f...") + val f = meta.qqq(u) + + println(s"apply f...") '{ (${ f })(${ Expr(v) }) } } + // invoke qqq without staging compiler + // this works because it is bypassing the staging compiler + inline def qq(u: String): (Double => Quantity[Double, Meter]) = + ${ meta.qq('u) } + object meta: import scala.language.implicitConversions + def qq(u: Expr[String])(using + Quotes + ): Expr[Double => Quantity[Double, Meter]] = + qqq(u.valueOrAbort) + + def qqq(u: String)(using + Quotes + ): Expr[Double => Quantity[Double, Meter]] = + import quotes.reflect.* + + // this eventually builds arbitrary unit expr via parsing + val t = u match + case "meter" => TypeRepr.of[Meter] + case "second" => TypeRepr.of[Second] + case _ => TypeRepr.of[1] + + println(s"t= $t") + + val ttt = t.asType match + case '[t] => Expr.summon[UnitConversion[Double, t, Meter]] + val cnv = ttt match + case Some(x) => + println(s"found ${x}") + x + case _ => + println(s"not found!") + null + + val f = t.asType match + case '[t] => + '{ (v: Double) => + v.withUnit[t] + .toUnit[Meter](using + ${ + cnv.asTerm + .asExprOf[UnitConversion[ + Double, + t, + Meter + ]] + } + ) + } + f + def fqtn[T](using Quotes, Type[T]): Expr[String] = import quotes.reflect.* val tr = TypeRepr.of[T] From df81ac0a93d8df9e80426352009aa8666966869d Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Tue, 17 Jan 2023 17:11:31 -0700 Subject: [PATCH 010/141] hardcode import works --- .../main/scala/coulomb/parser/parser.scala | 27 ++++--------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/parser/src/main/scala/coulomb/parser/parser.scala b/parser/src/main/scala/coulomb/parser/parser.scala index ead06dfcc..aa72aa91b 100644 --- a/parser/src/main/scala/coulomb/parser/parser.scala +++ b/parser/src/main/scala/coulomb/parser/parser.scala @@ -73,30 +73,13 @@ object test: println(s"t= $t") - val ttt = t.asType match - case '[t] => Expr.summon[UnitConversion[Double, t, Meter]] - val cnv = ttt match - case Some(x) => - println(s"found ${x}") - x - case _ => - println(s"not found!") - null - val f = t.asType match case '[t] => - '{ (v: Double) => - v.withUnit[t] - .toUnit[Meter](using - ${ - cnv.asTerm - .asExprOf[UnitConversion[ - Double, - t, - Meter - ]] - } - ) + '{ + // hard-coding imports works + // so question is how to allow library users to inject these from above + import coulomb.policy.standard.given + (v: Double) => v.withUnit[t].toUnit[Meter] } f From c8a1af4316ab7a5f3683fc8991642b580863f1e0 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 22 Jan 2023 13:12:33 -0700 Subject: [PATCH 011/141] poc string to TypeRef --- .../main/scala/coulomb/parser/parser.scala | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/parser/src/main/scala/coulomb/parser/parser.scala b/parser/src/main/scala/coulomb/parser/parser.scala index aa72aa91b..fc98a6d68 100644 --- a/parser/src/main/scala/coulomb/parser/parser.scala +++ b/parser/src/main/scala/coulomb/parser/parser.scala @@ -24,8 +24,6 @@ import coulomb.units.si.* import coulomb.conversion.* object test: - val stub = 0 - // fully qualified type name inline def fqtn[T]: String = ${ meta.fqtn[T] } @@ -52,9 +50,17 @@ object test: inline def qq(u: String): (Double => Quantity[Double, Meter]) = ${ meta.qq('u) } + inline def z(inline b: Int): Int = ${ meta.z('b) } + object meta: + import scala.unchecked import scala.language.implicitConversions + def z(b: Expr[Int])(using Quotes): Expr[Int] = + import quotes.reflect.* + println(s"${b.asTerm}") + b + def qq(u: Expr[String])(using Quotes ): Expr[Double => Quantity[Double, Meter]] = @@ -73,6 +79,24 @@ object test: println(s"t= $t") + val root = defn.RootPackage + println(s"root= $root") + println(s"root.DF= ${root.declaredFields}") + val clmb = root.declaredFields.filter(_.name == "coulomb").head + println(s"clmb= $clmb") + val units = clmb.declaredFields.filter(_.name == "units").head + val si = units.declaredFields.filter(_.name == "si").head + println(s"si= $si") + println(s"si.tree= ${si.tree}") + println(s"si.types= ${si.typeMembers}") + val meter = si.typeMembers.filter(_.name == "Meter").head + println(s"meter= $meter") + println(s"typeRef= ${meter.typeRef}") + + val tt = meter.typeRef + val iseq = (tt =:= t) + println(s"iseq= $iseq") + val f = t.asType match case '[t] => '{ From 2a98715cffdf94f0197eea85127eba8452e4f50a Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 22 Jan 2023 16:58:00 -0700 Subject: [PATCH 012/141] fqTypeRepr --- .../main/scala/coulomb/parser/parser.scala | 62 ++++++++++++++----- 1 file changed, 45 insertions(+), 17 deletions(-) diff --git a/parser/src/main/scala/coulomb/parser/parser.scala b/parser/src/main/scala/coulomb/parser/parser.scala index fc98a6d68..81c0135b8 100644 --- a/parser/src/main/scala/coulomb/parser/parser.scala +++ b/parser/src/main/scala/coulomb/parser/parser.scala @@ -79,23 +79,8 @@ object test: println(s"t= $t") - val root = defn.RootPackage - println(s"root= $root") - println(s"root.DF= ${root.declaredFields}") - val clmb = root.declaredFields.filter(_.name == "coulomb").head - println(s"clmb= $clmb") - val units = clmb.declaredFields.filter(_.name == "units").head - val si = units.declaredFields.filter(_.name == "si").head - println(s"si= $si") - println(s"si.tree= ${si.tree}") - println(s"si.types= ${si.typeMembers}") - val meter = si.typeMembers.filter(_.name == "Meter").head - println(s"meter= $meter") - println(s"typeRef= ${meter.typeRef}") - - val tt = meter.typeRef - val iseq = (tt =:= t) - println(s"iseq= $iseq") + val ttt = fqTypeRepr("coulomb.units.si.Meter") + assert(ttt =:= TypeRepr.of[coulomb.units.si.Meter]) val f = t.asType match case '[t] => @@ -107,6 +92,49 @@ object test: } f + def fqTypeRepr(using Quotes)( + path: Seq[String] + ): quotes.reflect.TypeRepr = + import quotes.reflect.* + if (path.isEmpty) + report.errorAndAbort("fqTypeRepr: empty path") + TypeRepr.of[Unit] + else + val q = fqFieldSymbol(path.dropRight(1)) + val qt = q.typeMembers.filter(_.name == path.last) + if (qt.length == 1) qt.head.typeRef + else + report.errorAndAbort( + s"""fqTypeRepr: bad path ${path.mkString(".")}""" + ) + TypeRepr.of[Unit] + + def fqTypeRepr(using Quotes)(path: String): quotes.reflect.TypeRepr = + fqTypeRepr(path.split('.').toIndexedSeq) + + def fqFieldSymbol(using Quotes)( + path: Seq[String] + ): quotes.reflect.Symbol = + import quotes.reflect.* + def work(q: Symbol, tail: Seq[String]): Symbol = + if (tail.isEmpty) q + else + val qt = q.declaredFields.filter(_.name == tail.head) + if (qt.length == 1) work(qt.head, tail.tail) + else + report.errorAndAbort( + s"""fqFieldSymbol: bad path ${path.mkString(".")}""" + ) + defn.RootPackage + path match + case _ if path.isEmpty => defn.RootPackage + case _ if path.head == "_root_" => + work(defn.RootPackage, path.tail) + case _ => work(defn.RootPackage, path) + + def fqFieldSymbol(using Quotes)(path: String): quotes.reflect.Symbol = + fqFieldSymbol(path.split('.').toIndexedSeq) + def fqtn[T](using Quotes, Type[T]): Expr[String] = import quotes.reflect.* val tr = TypeRepr.of[T] From de17011fa6236eeafbf35f1dca980252cb1702da Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Wed, 25 Jan 2023 18:59:14 -0700 Subject: [PATCH 013/141] symbolValueType --- .../main/scala/coulomb/parser/parser.scala | 58 ++++++++++++------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/parser/src/main/scala/coulomb/parser/parser.scala b/parser/src/main/scala/coulomb/parser/parser.scala index 81c0135b8..3d4cdc564 100644 --- a/parser/src/main/scala/coulomb/parser/parser.scala +++ b/parser/src/main/scala/coulomb/parser/parser.scala @@ -23,6 +23,16 @@ import coulomb.syntax.* import coulomb.units.si.* import coulomb.conversion.* +import coulomb.rational.Rational + +object ast: + sealed abstract class UnitAST + object UnitAST: + case class Unit(name: String) extends UnitAST + case class Mul(lhs: UnitAST, rhs: UnitAST) extends UnitAST + case class Div(num: UnitAST, den: UnitAST) extends UnitAST + case class Pow(b: UnitAST, e: Rational) extends UnitAST + object test: // fully qualified type name inline def fqtn[T]: String = ${ meta.fqtn[T] } @@ -50,17 +60,10 @@ object test: inline def qq(u: String): (Double => Quantity[Double, Meter]) = ${ meta.qq('u) } - inline def z(inline b: Int): Int = ${ meta.z('b) } - object meta: import scala.unchecked import scala.language.implicitConversions - def z(b: Expr[Int])(using Quotes): Expr[Int] = - import quotes.reflect.* - println(s"${b.asTerm}") - b - def qq(u: Expr[String])(using Quotes ): Expr[Double => Quantity[Double, Meter]] = @@ -73,14 +76,21 @@ object test: // this eventually builds arbitrary unit expr via parsing val t = u match - case "meter" => TypeRepr.of[Meter] - case "second" => TypeRepr.of[Second] - case _ => TypeRepr.of[1] - - println(s"t= $t") - - val ttt = fqTypeRepr("coulomb.units.si.Meter") - assert(ttt =:= TypeRepr.of[coulomb.units.si.Meter]) + case "meter" => fqTypeRepr("coulomb.units.si.Meter") + case "second" => fqTypeRepr("coulomb.units.si.Second") + case _ => TypeRepr.of[Unit] + + // val ttt = fqTypeRepr("coulomb.units.si.Meter") + // assert(ttt =:= TypeRepr.of[coulomb.units.si.Meter]) + + val s = fqFieldSymbol("coulomb.units.info.ctx_unit_Bit") + println(s"s= ${symbolValueType(s).show}") + println(s"s= ${symbolValueType(s).isSingleton}") + println(s"s= ${s.flags.show}") + val ss = fqFieldSymbol("coulomb.units.info") + println(s"ss= ${symbolValueType(ss).show}") + println(s"ss= ${symbolValueType(ss).isSingleton}") + println(s"ss= ${ss.flags.show}") val f = t.asType match case '[t] => @@ -92,6 +102,13 @@ object test: } f + def symbolValueType(using Quotes)( + sym: quotes.reflect.Symbol + ): quotes.reflect.TypeRepr = + import quotes.reflect.* + val TermRef(tr, _) = sym.termRef: @unchecked + tr.memberType(sym) + def fqTypeRepr(using Quotes)( path: Seq[String] ): quotes.reflect.TypeRepr = @@ -126,11 +143,12 @@ object test: s"""fqFieldSymbol: bad path ${path.mkString(".")}""" ) defn.RootPackage - path match - case _ if path.isEmpty => defn.RootPackage - case _ if path.head == "_root_" => - work(defn.RootPackage, path.tail) - case _ => work(defn.RootPackage, path) + if (path.isEmpty) + defn.RootPackage + else if (path.head == "_root_") + work(defn.RootPackage, path.tail) + else + work(defn.RootPackage, path) def fqFieldSymbol(using Quotes)(path: String): quotes.reflect.Symbol = fqFieldSymbol(path.split('.').toIndexedSeq) From 2722669c63f334083952a21afb37c36b48adf83f Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 29 Jan 2023 11:37:40 -0700 Subject: [PATCH 014/141] UnitAST --- .../main/scala/coulomb/parser/parser.scala | 268 +++++++++++------- 1 file changed, 165 insertions(+), 103 deletions(-) diff --git a/parser/src/main/scala/coulomb/parser/parser.scala b/parser/src/main/scala/coulomb/parser/parser.scala index 3d4cdc564..c1a4eab95 100644 --- a/parser/src/main/scala/coulomb/parser/parser.scala +++ b/parser/src/main/scala/coulomb/parser/parser.scala @@ -25,13 +25,34 @@ import coulomb.conversion.* import coulomb.rational.Rational -object ast: - sealed abstract class UnitAST - object UnitAST: - case class Unit(name: String) extends UnitAST - case class Mul(lhs: UnitAST, rhs: UnitAST) extends UnitAST - case class Div(num: UnitAST, den: UnitAST) extends UnitAST - case class Pow(b: UnitAST, e: Rational) extends UnitAST +final class RuntimeContext[UnitDefs] +object RuntimeContext: + final type &:[H, T] + final type TNil + + def apply[D](): RuntimeContext[D] = new RuntimeContext[D] + +object runtime: + import coulomb.define.* + import RuntimeContext.{&:, TNil} + import coulomb.units.si.* + import coulomb.units.si.prefixes.* + given given_context: RuntimeContext[ + BaseUnit[Meter, "meter", "m"] &: + DerivedUnit[Kilo, 10 ^ 3, "kilo", "k"] &: TNil + ] = RuntimeContext() + +sealed abstract class UnitAST: + def *(rhs: UnitAST): UnitAST.Mul = UnitAST.Mul(this, rhs) + def /(den: UnitAST): UnitAST.Div = UnitAST.Div(this, den) + def ^(e: Rational): UnitAST.Pow = UnitAST.Pow(this, e) + +object UnitAST: + case class UnitType(path: String) extends UnitAST + case class Mul(lhs: UnitAST, rhs: UnitAST) extends UnitAST + case class Div(num: UnitAST, den: UnitAST) extends UnitAST + case class Pow(b: UnitAST, e: Rational) extends UnitAST + inline def of[U]: UnitType = ${ meta.astunit[U] } object test: // fully qualified type name @@ -60,104 +81,145 @@ object test: inline def qq(u: String): (Double => Quantity[Double, Meter]) = ${ meta.qq('u) } - object meta: - import scala.unchecked - import scala.language.implicitConversions - - def qq(u: Expr[String])(using - Quotes - ): Expr[Double => Quantity[Double, Meter]] = - qqq(u.valueOrAbort) - - def qqq(u: String)(using - Quotes - ): Expr[Double => Quantity[Double, Meter]] = - import quotes.reflect.* - - // this eventually builds arbitrary unit expr via parsing - val t = u match - case "meter" => fqTypeRepr("coulomb.units.si.Meter") - case "second" => fqTypeRepr("coulomb.units.si.Second") - case _ => TypeRepr.of[Unit] - - // val ttt = fqTypeRepr("coulomb.units.si.Meter") - // assert(ttt =:= TypeRepr.of[coulomb.units.si.Meter]) - - val s = fqFieldSymbol("coulomb.units.info.ctx_unit_Bit") - println(s"s= ${symbolValueType(s).show}") - println(s"s= ${symbolValueType(s).isSingleton}") - println(s"s= ${s.flags.show}") - val ss = fqFieldSymbol("coulomb.units.info") - println(s"ss= ${symbolValueType(ss).show}") - println(s"ss= ${symbolValueType(ss).isSingleton}") - println(s"ss= ${ss.flags.show}") - - val f = t.asType match - case '[t] => - '{ - // hard-coding imports works - // so question is how to allow library users to inject these from above - import coulomb.policy.standard.given - (v: Double) => v.withUnit[t].toUnit[Meter] - } - f - - def symbolValueType(using Quotes)( - sym: quotes.reflect.Symbol - ): quotes.reflect.TypeRepr = - import quotes.reflect.* - val TermRef(tr, _) = sym.termRef: @unchecked - tr.memberType(sym) - - def fqTypeRepr(using Quotes)( - path: Seq[String] - ): quotes.reflect.TypeRepr = - import quotes.reflect.* - if (path.isEmpty) - report.errorAndAbort("fqTypeRepr: empty path") + inline def bu: Unit = + ${ meta.bu } + +object meta: + import scala.unchecked + import scala.language.implicitConversions + + def bu(using Quotes): Expr[Unit] = + import quotes.reflect.* + val m = baseunittree(fqTypeRepr("coulomb.units.si.Meter")) + println(s"m= $m") + println(s"m= ${m.symbol.fullName}") + val Ident(s) = m: @unchecked + println(s"s= $s") + val t = symbolValueType(fqFieldSymbol(m.symbol.fullName)) + println(s"t= $t") + '{ () } + + def baseunittree(using Quotes)( + u: quotes.reflect.TypeRepr + ): quotes.reflect.Tree = + import quotes.reflect.* + Implicits.search( + TypeRepr + .of[coulomb.define.BaseUnit] + .appliedTo(List(u, TypeBounds.empty, TypeBounds.empty)) + ) match + case iss: ImplicitSearchSuccess => iss.tree + case _ => Literal(UnitConstant()) + + def qq(u: Expr[String])(using + Quotes + ): Expr[Double => Quantity[Double, Meter]] = + qqq(u.valueOrAbort) + + def qqq(u: String)(using + Quotes + ): Expr[Double => Quantity[Double, Meter]] = + import quotes.reflect.* + + // this eventually builds arbitrary unit expr via parsing + val t = u match + case "meter" => fqTypeRepr("coulomb.units.si.Meter") + case "second" => fqTypeRepr("coulomb.units.si.Second") + case _ => TypeRepr.of[Unit] + + // val ttt = fqTypeRepr("coulomb.units.si.Meter") + // assert(ttt =:= TypeRepr.of[coulomb.units.si.Meter]) + + val s = fqFieldSymbol("coulomb.units.info.ctx_unit_Bit") + println(s"s= ${symbolValueType(s).show}") + println(s"s= ${symbolValueType(s).isSingleton}") + println(s"s= ${s.flags.show}") + val ss = fqFieldSymbol("coulomb.units.info") + println(s"ss= ${symbolValueType(ss).show}") + println(s"ss= ${symbolValueType(ss).isSingleton}") + println(s"ss= ${ss.flags.show}") + + val g = '{ given given_test: String = "foooooo" } + println(s"g= ${g.show}") + + val f = t.asType match + case '[t] => + '{ + // hard-coding imports works + // so question is how to allow library users to inject these from above + import coulomb.policy.standard.given + (v: Double) => v.withUnit[t].toUnit[Meter] + } + f + + def symbolValueType(using Quotes)( + sym: quotes.reflect.Symbol + ): quotes.reflect.TypeRepr = + import quotes.reflect.* + val TermRef(tr, _) = sym.termRef: @unchecked + tr.memberType(sym) + + def fqTypeRepr(using Quotes)( + path: Seq[String] + ): quotes.reflect.TypeRepr = + import quotes.reflect.* + if (path.isEmpty) + report.errorAndAbort("fqTypeRepr: empty path") + TypeRepr.of[Unit] + else + val q = fqFieldSymbol(path.dropRight(1)) + val qt = q.typeMembers.filter(_.name == path.last) + if (qt.length == 1) qt.head.typeRef + else + report.errorAndAbort( + s"""fqTypeRepr: bad path ${path.mkString( + "." + )} at ${path.last}""" + ) TypeRepr.of[Unit] + + def fqTypeRepr(using Quotes)(path: String): quotes.reflect.TypeRepr = + fqTypeRepr(path.split('.').toIndexedSeq) + + def fqFieldSymbol(using Quotes)( + path: Seq[String] + ): quotes.reflect.Symbol = + import quotes.reflect.* + def work(q: Symbol, tail: Seq[String]): Symbol = + if (tail.isEmpty) q else - val q = fqFieldSymbol(path.dropRight(1)) - val qt = q.typeMembers.filter(_.name == path.last) - if (qt.length == 1) qt.head.typeRef + val qt = q.declaredFields.filter(_.name == tail.head) + if (qt.length == 1) work(qt.head, tail.tail) else report.errorAndAbort( - s"""fqTypeRepr: bad path ${path.mkString(".")}""" + s"""fqFieldSymbol: bad path ${path.mkString( + "." + )} at ${tail.head}""" ) - TypeRepr.of[Unit] - - def fqTypeRepr(using Quotes)(path: String): quotes.reflect.TypeRepr = - fqTypeRepr(path.split('.').toIndexedSeq) - - def fqFieldSymbol(using Quotes)( - path: Seq[String] - ): quotes.reflect.Symbol = - import quotes.reflect.* - def work(q: Symbol, tail: Seq[String]): Symbol = - if (tail.isEmpty) q - else - val qt = q.declaredFields.filter(_.name == tail.head) - if (qt.length == 1) work(qt.head, tail.tail) - else - report.errorAndAbort( - s"""fqFieldSymbol: bad path ${path.mkString(".")}""" - ) - defn.RootPackage - if (path.isEmpty) - defn.RootPackage - else if (path.head == "_root_") - work(defn.RootPackage, path.tail) - else - work(defn.RootPackage, path) - - def fqFieldSymbol(using Quotes)(path: String): quotes.reflect.Symbol = - fqFieldSymbol(path.split('.').toIndexedSeq) - - def fqtn[T](using Quotes, Type[T]): Expr[String] = - import quotes.reflect.* - val tr = TypeRepr.of[T] - Expr(tr.dealias.typeSymbol.fullName) - - def m[T](using Quotes, Type[T]): Expr[Map[String, String]] = - val mm = Map("a" -> "b") - Expr(mm) + defn.RootPackage + if (path.isEmpty) + defn.RootPackage + else if (path.head == "_root_") + work(defn.RootPackage, path.tail) + else + work(defn.RootPackage, path) + + def fqFieldSymbol(using Quotes)(path: String): quotes.reflect.Symbol = + fqFieldSymbol(path.split('.').toIndexedSeq) + + def astunit[T](using Quotes, Type[T]): Expr[UnitAST.UnitType] = + import quotes.reflect.* + val tr = TypeRepr.of[T] + // filtering is a hack - these dollar sign variant symbols do not + // show up in my fqTypRepr navigation + val path = tr.dealias.typeSymbol.fullName.filter(_ != '$') + '{ UnitAST.UnitType(${ Expr(path) }) } + + def fqtn[T](using Quotes, Type[T]): Expr[String] = + import quotes.reflect.* + val tr = TypeRepr.of[T] + Expr(tr.dealias.typeSymbol.fullName) + + def m[T](using Quotes, Type[T]): Expr[Map[String, String]] = + val mm = Map("a" -> "b") + Expr(mm) From b43d85bce3936ecdc5b208e4085f2b49b06109f9 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Mon, 30 Jan 2023 17:00:20 -0700 Subject: [PATCH 015/141] widen --- .../main/scala/coulomb/parser/parser.scala | 63 +++++++++++++++---- 1 file changed, 51 insertions(+), 12 deletions(-) diff --git a/parser/src/main/scala/coulomb/parser/parser.scala b/parser/src/main/scala/coulomb/parser/parser.scala index c1a4eab95..eb3522e87 100644 --- a/parser/src/main/scala/coulomb/parser/parser.scala +++ b/parser/src/main/scala/coulomb/parser/parser.scala @@ -93,10 +93,32 @@ object meta: val m = baseunittree(fqTypeRepr("coulomb.units.si.Meter")) println(s"m= $m") println(s"m= ${m.symbol.fullName}") - val Ident(s) = m: @unchecked - println(s"s= $s") val t = symbolValueType(fqFieldSymbol(m.symbol.fullName)) - println(s"t= $t") + println(s"t= ${t.show}") + /* + val s2 = fqFieldSymbol("coulomb.units.si.ctx_unit_Meter") + val s3 = fqFieldSymbol("coulomb.units.us.ctx_unit_Meter") + println(s"${s2.tree.show}") + println(s"${s3.tree.show}") + println(s"${s2.typeRef}") + println(s"${s3.typeRef}") + println(s"${s2.termRef}") + println(s"${s3.termRef}") + val r2 = Ref.term(s2.termRef) + println(s"$r2") + val r3 = Ref.term(s3.termRef) + println(s"$r3") + val u2 = r2.underlying + println(s"${u2.show}") + val u3 = r3.underlying + println(s"${u3.show}") + val t2 = symbolValueType(fqFieldSymbol("coulomb.units.si$.ctx_unit_Meter")) + println(s"${t2.show}") + println(s"${t2.widen.show}") + val t3 = symbolValueType(fqFieldSymbol("coulomb.units.us$.ctx_unit_Meter")) + println(s"${t3.show}") + println(s"${t3.widen.show}") + */ '{ () } def baseunittree(using Quotes)( @@ -157,7 +179,7 @@ object meta: ): quotes.reflect.TypeRepr = import quotes.reflect.* val TermRef(tr, _) = sym.termRef: @unchecked - tr.memberType(sym) + tr.memberType(sym).widen def fqTypeRepr(using Quotes)( path: Seq[String] @@ -188,15 +210,32 @@ object meta: def work(q: Symbol, tail: Seq[String]): Symbol = if (tail.isEmpty) q else - val qt = q.declaredFields.filter(_.name == tail.head) - if (qt.length == 1) work(qt.head, tail.tail) + // println(s"\nsymbol $q") + // look for modules first + // this includes packages and objects + val qt = q.declarations.filter { x => + (x.name == tail.head) && x.flags.is(Flags.Module) + } + // println(s"${qt.map{x => (x, x.flags.show)}}") + val tt = q.declaredFields.filter(_.name == tail.head) + // println(s"${tt.map{x => (x, x.flags.show)}}") + // there are sometimes two symbols representing a module + // is the difference important to this function? + if (qt.length > 0) work(qt.head, tail.tail) else - report.errorAndAbort( - s"""fqFieldSymbol: bad path ${path.mkString( - "." - )} at ${tail.head}""" - ) - defn.RootPackage + // if no module exists, look for declared symbol + val f = q.declarations.filter { x => x.name == tail.head } + // println(s"${f.map{x => (x, x.flags.show)}}") + // expect a unique field declaration in this case + if ((f.length == 1) && (tail.length == 1)) f.head + else + // if we cannot find a field here, it is a failure + report.errorAndAbort( + s"""fqFieldSymbol: bad path ${path.mkString( + "." + )} at ${tail.head}""" + ) + defn.RootPackage if (path.isEmpty) defn.RootPackage else if (path.head == "_root_") From e7c266e67a1631b697fee3d10b73327649439775 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Mon, 30 Jan 2023 18:45:24 -0700 Subject: [PATCH 016/141] astTypeRepr --- .../main/scala/coulomb/parser/parser.scala | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/parser/src/main/scala/coulomb/parser/parser.scala b/parser/src/main/scala/coulomb/parser/parser.scala index eb3522e87..a257d1d4d 100644 --- a/parser/src/main/scala/coulomb/parser/parser.scala +++ b/parser/src/main/scala/coulomb/parser/parser.scala @@ -21,6 +21,7 @@ import scala.quoted.* import coulomb.* import coulomb.syntax.* import coulomb.units.si.* +import coulomb.units.us.* import coulomb.conversion.* import coulomb.rational.Rational @@ -60,6 +61,24 @@ object test: inline def m[T]: Map[String, String] = ${ meta.m[T] } + def s(astf: UnitAST, astt: UnitAST)(using staging.Compiler): Double = + staging.run { + import quotes.reflect.* + + val tf = meta.astTypeRepr(astf) + val tt = meta.astTypeRepr(astt) + + println(s"tf= ${tf.show}") + println(s"tt= ${tt.show}") + + val r = (tf.asType, tt.asType) match + case ('[f], '[t]) => + '{ coulomb.conversion.coefficients.coefficientDouble[f, t] } + + println("hi") + r + } + // this compiles and "runs" but run-time fails trying to find // coulomb implicits - so the staging compiler // currently doesn't work for finding imported implicits @@ -174,6 +193,22 @@ object meta: } f + def astTypeRepr(using Quotes)( + ast: UnitAST + ): quotes.reflect.TypeRepr = + import quotes.reflect.* + ast match + case UnitAST.UnitType(path) => fqTypeRepr(path) + case UnitAST.Mul(l, r) => + val ltr = astTypeRepr(l) + val rtr = astTypeRepr(r) + TypeRepr.of[coulomb.`*`].appliedTo(List(ltr, rtr)) + case UnitAST.Div(n, d) => + val ntr = astTypeRepr(n) + val dtr = astTypeRepr(d) + TypeRepr.of[coulomb.`/`].appliedTo(List(ntr, dtr)) + case UnitAST.Pow(b, e) => astTypeRepr(b) + def symbolValueType(using Quotes)( sym: quotes.reflect.Symbol ): quotes.reflect.TypeRepr = From 31fb3ad4427556bc1e86566b4c1befd886a2e2a8 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Tue, 31 Jan 2023 12:44:58 -0700 Subject: [PATCH 017/141] kernel --- .../main/scala/coulomb/parser/parser.scala | 211 +++++------------- 1 file changed, 52 insertions(+), 159 deletions(-) diff --git a/parser/src/main/scala/coulomb/parser/parser.scala b/parser/src/main/scala/coulomb/parser/parser.scala index a257d1d4d..1c810500d 100644 --- a/parser/src/main/scala/coulomb/parser/parser.scala +++ b/parser/src/main/scala/coulomb/parser/parser.scala @@ -20,29 +20,8 @@ import scala.quoted.* import coulomb.* import coulomb.syntax.* -import coulomb.units.si.* -import coulomb.units.us.* -import coulomb.conversion.* - import coulomb.rational.Rational -final class RuntimeContext[UnitDefs] -object RuntimeContext: - final type &:[H, T] - final type TNil - - def apply[D](): RuntimeContext[D] = new RuntimeContext[D] - -object runtime: - import coulomb.define.* - import RuntimeContext.{&:, TNil} - import coulomb.units.si.* - import coulomb.units.si.prefixes.* - given given_context: RuntimeContext[ - BaseUnit[Meter, "meter", "m"] &: - DerivedUnit[Kilo, 10 ^ 3, "kilo", "k"] &: TNil - ] = RuntimeContext() - sealed abstract class UnitAST: def *(rhs: UnitAST): UnitAST.Mul = UnitAST.Mul(this, rhs) def /(den: UnitAST): UnitAST.Div = UnitAST.Div(this, den) @@ -53,92 +32,36 @@ object UnitAST: case class Mul(lhs: UnitAST, rhs: UnitAST) extends UnitAST case class Div(num: UnitAST, den: UnitAST) extends UnitAST case class Pow(b: UnitAST, e: Rational) extends UnitAST - inline def of[U]: UnitType = ${ meta.astunit[U] } - -object test: - // fully qualified type name - inline def fqtn[T]: String = ${ meta.fqtn[T] } - - inline def m[T]: Map[String, String] = ${ meta.m[T] } - - def s(astf: UnitAST, astt: UnitAST)(using staging.Compiler): Double = - staging.run { - import quotes.reflect.* - - val tf = meta.astTypeRepr(astf) - val tt = meta.astTypeRepr(astt) - - println(s"tf= ${tf.show}") - println(s"tt= ${tt.show}") - - val r = (tf.asType, tt.asType) match - case ('[f], '[t]) => - '{ coulomb.conversion.coefficients.coefficientDouble[f, t] } - - println("hi") - r - } - - // this compiles and "runs" but run-time fails trying to find - // coulomb implicits - so the staging compiler - // currently doesn't work for finding imported implicits - // unsure if this is due to bad class loader or something else - def q(v: Double, u: String)(using - staging.Compiler - ): Quantity[Double, Meter] = staging.run { - // inside this scope Quotes is defined - - println(s"get f...") - val f = meta.qqq(u) - - println(s"apply f...") - '{ (${ f })(${ Expr(v) }) } - } - - // invoke qqq without staging compiler - // this works because it is bypassing the staging compiler - inline def qq(u: String): (Double => Quantity[Double, Meter]) = - ${ meta.qq('u) } - - inline def bu: Unit = - ${ meta.bu } + inline def of[U]: UnitAST = ${ meta.unitAST[U] } object meta: import scala.unchecked import scala.language.implicitConversions - def bu(using Quotes): Expr[Unit] = - import quotes.reflect.* - val m = baseunittree(fqTypeRepr("coulomb.units.si.Meter")) - println(s"m= $m") - println(s"m= ${m.symbol.fullName}") - val t = symbolValueType(fqFieldSymbol(m.symbol.fullName)) - println(s"t= ${t.show}") - /* - val s2 = fqFieldSymbol("coulomb.units.si.ctx_unit_Meter") - val s3 = fqFieldSymbol("coulomb.units.us.ctx_unit_Meter") - println(s"${s2.tree.show}") - println(s"${s3.tree.show}") - println(s"${s2.typeRef}") - println(s"${s3.typeRef}") - println(s"${s2.termRef}") - println(s"${s3.termRef}") - val r2 = Ref.term(s2.termRef) - println(s"$r2") - val r3 = Ref.term(s3.termRef) - println(s"$r3") - val u2 = r2.underlying - println(s"${u2.show}") - val u3 = r3.underlying - println(s"${u3.show}") - val t2 = symbolValueType(fqFieldSymbol("coulomb.units.si$.ctx_unit_Meter")) - println(s"${t2.show}") - println(s"${t2.widen.show}") - val t3 = symbolValueType(fqFieldSymbol("coulomb.units.us$.ctx_unit_Meter")) - println(s"${t3.show}") - println(s"${t3.widen.show}") - */ - '{ () } + import coulomb.conversion.coefficients.{meta => _, *} + import coulomb.infra.meta.{*, given} + + given ctx_UnitASTToExpr: ToExpr[UnitAST] with + def apply(ast: UnitAST)(using Quotes): Expr[UnitAST] = + ast match + case UnitAST.UnitType(path) => + '{ UnitAST.UnitType(${ Expr(path) }) } + case UnitAST.Mul(l, r) => + '{ UnitAST.Mul(${ Expr(l) }, ${ Expr(r) }) } + case UnitAST.Div(n, d) => + '{ UnitAST.Div(${ Expr(n) }, ${ Expr(d) }) } + case UnitAST.Pow(b, e) => + '{ UnitAST.Pow(${ Expr(b) }, ${ Expr(e) }) } + + def kernel(v: Rational, astF: UnitAST, astT: UnitAST)(using + staging.Compiler + ): Rational = + staging.run { + import quotes.reflect.* + (astTypeRepr(astF).asType, astTypeRepr(astT).asType) match + case ('[uf], '[ut]) => + '{ ${ Expr(v) } * coefficientRational[uf, ut] } + } def baseunittree(using Quotes)( u: quotes.reflect.TypeRepr @@ -152,46 +75,9 @@ object meta: case iss: ImplicitSearchSuccess => iss.tree case _ => Literal(UnitConstant()) - def qq(u: Expr[String])(using - Quotes - ): Expr[Double => Quantity[Double, Meter]] = - qqq(u.valueOrAbort) - - def qqq(u: String)(using - Quotes - ): Expr[Double => Quantity[Double, Meter]] = + def unitAST[U](using Quotes, Type[U]): Expr[UnitAST] = import quotes.reflect.* - - // this eventually builds arbitrary unit expr via parsing - val t = u match - case "meter" => fqTypeRepr("coulomb.units.si.Meter") - case "second" => fqTypeRepr("coulomb.units.si.Second") - case _ => TypeRepr.of[Unit] - - // val ttt = fqTypeRepr("coulomb.units.si.Meter") - // assert(ttt =:= TypeRepr.of[coulomb.units.si.Meter]) - - val s = fqFieldSymbol("coulomb.units.info.ctx_unit_Bit") - println(s"s= ${symbolValueType(s).show}") - println(s"s= ${symbolValueType(s).isSingleton}") - println(s"s= ${s.flags.show}") - val ss = fqFieldSymbol("coulomb.units.info") - println(s"ss= ${symbolValueType(ss).show}") - println(s"ss= ${symbolValueType(ss).isSingleton}") - println(s"ss= ${ss.flags.show}") - - val g = '{ given given_test: String = "foooooo" } - println(s"g= ${g.show}") - - val f = t.asType match - case '[t] => - '{ - // hard-coding imports works - // so question is how to allow library users to inject these from above - import coulomb.policy.standard.given - (v: Double) => v.withUnit[t].toUnit[Meter] - } - f + Expr(typeReprAST(TypeRepr.of[U])) def astTypeRepr(using Quotes)( ast: UnitAST @@ -207,7 +93,31 @@ object meta: val ntr = astTypeRepr(n) val dtr = astTypeRepr(d) TypeRepr.of[coulomb.`/`].appliedTo(List(ntr, dtr)) - case UnitAST.Pow(b, e) => astTypeRepr(b) + case UnitAST.Pow(b, e) => + val btr = astTypeRepr(b) + val etr = rationalTE(e) + TypeRepr.of[coulomb.`^`].appliedTo(List(btr, etr)) + + def typeReprAST(using Quotes)( + tr: quotes.reflect.TypeRepr + ): UnitAST = + import quotes.reflect.* + tr match + case AppliedType(op, List(lu, ru)) + if (op =:= TypeRepr.of[coulomb.`*`]) => + UnitAST.Mul(typeReprAST(lu), typeReprAST(ru)) + case AppliedType(op, List(lu, ru)) + if (op =:= TypeRepr.of[coulomb.`/`]) => + UnitAST.Div(typeReprAST(lu), typeReprAST(ru)) + case AppliedType(op, List(b, e)) + if (op =:= TypeRepr.of[coulomb.`^`]) => + val rationalTE(ev) = e: @unchecked + UnitAST.Pow(typeReprAST(b), ev) + case t => + // should add checking for types with type-args here + // possibly an explicit non dealiasting policy here would allow + // parameterized types to be handled via typedef aliases? + UnitAST.UnitType(t.typeSymbol.fullName) def symbolValueType(using Quotes)( sym: quotes.reflect.Symbol @@ -280,20 +190,3 @@ object meta: def fqFieldSymbol(using Quotes)(path: String): quotes.reflect.Symbol = fqFieldSymbol(path.split('.').toIndexedSeq) - - def astunit[T](using Quotes, Type[T]): Expr[UnitAST.UnitType] = - import quotes.reflect.* - val tr = TypeRepr.of[T] - // filtering is a hack - these dollar sign variant symbols do not - // show up in my fqTypRepr navigation - val path = tr.dealias.typeSymbol.fullName.filter(_ != '$') - '{ UnitAST.UnitType(${ Expr(path) }) } - - def fqtn[T](using Quotes, Type[T]): Expr[String] = - import quotes.reflect.* - val tr = TypeRepr.of[T] - Expr(tr.dealias.typeSymbol.fullName) - - def m[T](using Quotes, Type[T]): Expr[Map[String, String]] = - val mm = Map("a" -> "b") - Expr(mm) From 34bd4621888eee7ec0e5635695228ac742759877 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Tue, 31 Jan 2023 15:14:09 -0700 Subject: [PATCH 018/141] bridge static -> runtime --- build.sbt | 10 +++---- .../main/scala/coulomb/runtime/runtime.scala | 30 ++++++++++++++++++- 2 files changed, 34 insertions(+), 6 deletions(-) rename parser/src/main/scala/coulomb/parser/parser.scala => runtime/src/main/scala/coulomb/runtime/runtime.scala (88%) diff --git a/build.sbt b/build.sbt index a59ae72cf..5eac194b6 100644 --- a/build.sbt +++ b/build.sbt @@ -38,7 +38,7 @@ def commonSettings = Seq( ) lazy val root = tlCrossRootProject - .aggregate(core, units, parser, spire, refined, testkit, unidocs) + .aggregate(core, units, runtime, spire, refined, testkit, unidocs) lazy val core = crossProject(JVMPlatform, JSPlatform, NativePlatform) .crossType(CrossType.Pure) @@ -61,10 +61,10 @@ lazy val units = crossProject(JVMPlatform, JSPlatform, NativePlatform) ) // see also: https://github.com/lampepfl/dotty/issues/7647 -lazy val parser = crossProject(JVMPlatform, JSPlatform, NativePlatform) +lazy val runtime = crossProject(JVMPlatform, JSPlatform, NativePlatform) .crossType(CrossType.Pure) - .in(file("parser")) - .settings(name := "coulomb-parser") + .in(file("runtime")) + .settings(name := "coulomb-runtime") .dependsOn( core % "compile->compile;test->test", units @@ -117,7 +117,7 @@ lazy val all = project .dependsOn( core.jvm, units.jvm, - parser.jvm, + runtime.jvm, spire.jvm, refined.jvm ) // scala repl only needs JVMPlatform subproj builds diff --git a/parser/src/main/scala/coulomb/parser/parser.scala b/runtime/src/main/scala/coulomb/runtime/runtime.scala similarity index 88% rename from parser/src/main/scala/coulomb/parser/parser.scala rename to runtime/src/main/scala/coulomb/runtime/runtime.scala index 1c810500d..36c15a809 100644 --- a/parser/src/main/scala/coulomb/parser/parser.scala +++ b/runtime/src/main/scala/coulomb/runtime/runtime.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package coulomb.parser +package coulomb.runtime import scala.quoted.* @@ -34,6 +34,10 @@ object UnitAST: case class Pow(b: UnitAST, e: Rational) extends UnitAST inline def of[U]: UnitAST = ${ meta.unitAST[U] } +object test: + inline def tk[U](v: Rational, u: UnitAST): Quantity[Rational, U] = + ${ meta.tk[U]('v, 'u) } + object meta: import scala.unchecked import scala.language.implicitConversions @@ -53,6 +57,21 @@ object meta: case UnitAST.Pow(b, e) => '{ UnitAST.Pow(${ Expr(b) }, ${ Expr(e) }) } + def tk[U](v: Expr[Rational], u: Expr[UnitAST])(using + Quotes, + Type[U] + ): Expr[Quantity[Rational, U]] = + import quotes.reflect.* + val cmp = stagingCompiler + println(s"cmp= ${cmp.asTerm}") + val astU = typeReprAST(TypeRepr.of[U]) + println(s"astU= $astU") + val expr = '{ + kernel($v, $u, ${ Expr(astU) })(using $cmp).withUnit[U] + } + println(s"expr= ${expr.asTerm.show}") + expr + def kernel(v: Rational, astF: UnitAST, astT: UnitAST)(using staging.Compiler ): Rational = @@ -63,6 +82,15 @@ object meta: '{ ${ Expr(v) } * coefficientRational[uf, ut] } } + def stagingCompiler(using Quotes): Expr[staging.Compiler] = + import quotes.reflect.* + Implicits.search(TypeRepr.of[staging.Compiler]) match + case iss: ImplicitSearchSuccess => + iss.tree.asExprOf[staging.Compiler] + case _ => + report.errorAndAbort("no.") + null.asInstanceOf[Expr[staging.Compiler]] + def baseunittree(using Quotes)( u: quotes.reflect.TypeRepr ): quotes.reflect.Tree = From fbc69c152aac55b4b85fa85ac0af514cd9a49111 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Tue, 31 Jan 2023 16:48:56 -0700 Subject: [PATCH 019/141] withUnitRuntime --- .../main/scala/coulomb/runtime/runtime.scala | 67 +++++++------------ 1 file changed, 24 insertions(+), 43 deletions(-) diff --git a/runtime/src/main/scala/coulomb/runtime/runtime.scala b/runtime/src/main/scala/coulomb/runtime/runtime.scala index 36c15a809..c606e7d70 100644 --- a/runtime/src/main/scala/coulomb/runtime/runtime.scala +++ b/runtime/src/main/scala/coulomb/runtime/runtime.scala @@ -34,9 +34,16 @@ object UnitAST: case class Pow(b: UnitAST, e: Rational) extends UnitAST inline def of[U]: UnitAST = ${ meta.unitAST[U] } -object test: - inline def tk[U](v: Rational, u: UnitAST): Quantity[Rational, U] = - ${ meta.tk[U]('v, 'u) } +package syntax { + import coulomb.conversion.* + + extension [V](v: V) + inline def withUnitRuntime[U](u: UnitAST)(using + vi: ValueConversion[V, Rational], + vo: ValueConversion[Rational, V] + ): Quantity[V, U] = + vo(meta.kernelExpr[U](vi(v), u)).withUnit[U] +} object meta: import scala.unchecked @@ -57,22 +64,19 @@ object meta: case UnitAST.Pow(b, e) => '{ UnitAST.Pow(${ Expr(b) }, ${ Expr(e) }) } - def tk[U](v: Expr[Rational], u: Expr[UnitAST])(using + inline def kernelExpr[U](v: Rational, u: UnitAST): Rational = + ${ kernelExprMeta[U]('v, 'u) } + + def kernelExprMeta[U](v: Expr[Rational], u: Expr[UnitAST])(using Quotes, Type[U] - ): Expr[Quantity[Rational, U]] = + ): Expr[Rational] = import quotes.reflect.* val cmp = stagingCompiler - println(s"cmp= ${cmp.asTerm}") val astU = typeReprAST(TypeRepr.of[U]) - println(s"astU= $astU") - val expr = '{ - kernel($v, $u, ${ Expr(astU) })(using $cmp).withUnit[U] - } - println(s"expr= ${expr.asTerm.show}") - expr + '{ kernelRuntime($v, $u, ${ Expr(astU) })(using $cmp) } - def kernel(v: Rational, astF: UnitAST, astT: UnitAST)(using + def kernelRuntime(v: Rational, astF: UnitAST, astT: UnitAST)(using staging.Compiler ): Rational = staging.run { @@ -88,21 +92,10 @@ object meta: case iss: ImplicitSearchSuccess => iss.tree.asExprOf[staging.Compiler] case _ => - report.errorAndAbort("no.") + report.errorAndAbort("no 'given' staging.Compiler is in scope") + // I'm not even sorry. null.asInstanceOf[Expr[staging.Compiler]] - def baseunittree(using Quotes)( - u: quotes.reflect.TypeRepr - ): quotes.reflect.Tree = - import quotes.reflect.* - Implicits.search( - TypeRepr - .of[coulomb.define.BaseUnit] - .appliedTo(List(u, TypeBounds.empty, TypeBounds.empty)) - ) match - case iss: ImplicitSearchSuccess => iss.tree - case _ => Literal(UnitConstant()) - def unitAST[U](using Quotes, Type[U]): Expr[UnitAST] = import quotes.reflect.* Expr(typeReprAST(TypeRepr.of[U])) @@ -147,12 +140,11 @@ object meta: // parameterized types to be handled via typedef aliases? UnitAST.UnitType(t.typeSymbol.fullName) - def symbolValueType(using Quotes)( - sym: quotes.reflect.Symbol - ): quotes.reflect.TypeRepr = - import quotes.reflect.* - val TermRef(tr, _) = sym.termRef: @unchecked - tr.memberType(sym).widen + def fqTypeRepr(using Quotes)(path: String): quotes.reflect.TypeRepr = + fqTypeRepr(path.split('.').toIndexedSeq) + + def fqFieldSymbol(using Quotes)(path: String): quotes.reflect.Symbol = + fqFieldSymbol(path.split('.').toIndexedSeq) def fqTypeRepr(using Quotes)( path: Seq[String] @@ -173,9 +165,6 @@ object meta: ) TypeRepr.of[Unit] - def fqTypeRepr(using Quotes)(path: String): quotes.reflect.TypeRepr = - fqTypeRepr(path.split('.').toIndexedSeq) - def fqFieldSymbol(using Quotes)( path: Seq[String] ): quotes.reflect.Symbol = @@ -183,22 +172,17 @@ object meta: def work(q: Symbol, tail: Seq[String]): Symbol = if (tail.isEmpty) q else - // println(s"\nsymbol $q") // look for modules first // this includes packages and objects val qt = q.declarations.filter { x => (x.name == tail.head) && x.flags.is(Flags.Module) } - // println(s"${qt.map{x => (x, x.flags.show)}}") - val tt = q.declaredFields.filter(_.name == tail.head) - // println(s"${tt.map{x => (x, x.flags.show)}}") // there are sometimes two symbols representing a module // is the difference important to this function? if (qt.length > 0) work(qt.head, tail.tail) else // if no module exists, look for declared symbol val f = q.declarations.filter { x => x.name == tail.head } - // println(s"${f.map{x => (x, x.flags.show)}}") // expect a unique field declaration in this case if ((f.length == 1) && (tail.length == 1)) f.head else @@ -215,6 +199,3 @@ object meta: work(defn.RootPackage, path.tail) else work(defn.RootPackage, path) - - def fqFieldSymbol(using Quotes)(path: String): quotes.reflect.Symbol = - fqFieldSymbol(path.split('.').toIndexedSeq) From 34a1aad650f4c08b472541c804713b99c9f0b6f5 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Wed, 1 Feb 2023 06:57:15 -0700 Subject: [PATCH 020/141] UnitAST -> RuntimeUnit, return Either --- .../main/scala/coulomb/runtime/runtime.scala | 112 +++++++++--------- 1 file changed, 58 insertions(+), 54 deletions(-) diff --git a/runtime/src/main/scala/coulomb/runtime/runtime.scala b/runtime/src/main/scala/coulomb/runtime/runtime.scala index c606e7d70..8380fd4ef 100644 --- a/runtime/src/main/scala/coulomb/runtime/runtime.scala +++ b/runtime/src/main/scala/coulomb/runtime/runtime.scala @@ -22,27 +22,31 @@ import coulomb.* import coulomb.syntax.* import coulomb.rational.Rational -sealed abstract class UnitAST: - def *(rhs: UnitAST): UnitAST.Mul = UnitAST.Mul(this, rhs) - def /(den: UnitAST): UnitAST.Div = UnitAST.Div(this, den) - def ^(e: Rational): UnitAST.Pow = UnitAST.Pow(this, e) - -object UnitAST: - case class UnitType(path: String) extends UnitAST - case class Mul(lhs: UnitAST, rhs: UnitAST) extends UnitAST - case class Div(num: UnitAST, den: UnitAST) extends UnitAST - case class Pow(b: UnitAST, e: Rational) extends UnitAST - inline def of[U]: UnitAST = ${ meta.unitAST[U] } +sealed abstract class RuntimeUnit: + def *(rhs: RuntimeUnit): RuntimeUnit.Mul = RuntimeUnit.Mul(this, rhs) + def /(den: RuntimeUnit): RuntimeUnit.Div = RuntimeUnit.Div(this, den) + def ^(e: Rational): RuntimeUnit.Pow = RuntimeUnit.Pow(this, e) + +object RuntimeUnit: + case class UnitType(path: String) extends RuntimeUnit + case class Mul(lhs: RuntimeUnit, rhs: RuntimeUnit) extends RuntimeUnit + case class Div(num: RuntimeUnit, den: RuntimeUnit) extends RuntimeUnit + case class Pow(b: RuntimeUnit, e: Rational) extends RuntimeUnit + inline def of[U]: RuntimeUnit = ${ meta.unitRTU[U] } package syntax { + import scala.util.{Try, Success, Failure} import coulomb.conversion.* - extension [V](v: V) - inline def withUnitRuntime[U](u: UnitAST)(using + extension [V](vu: (V, RuntimeUnit)) + inline def toQuantity[U](using vi: ValueConversion[V, Rational], vo: ValueConversion[Rational, V] - ): Quantity[V, U] = - vo(meta.kernelExpr[U](vi(v), u)).withUnit[U] + ): Either[String, Quantity[V, U]] = + val (v, u) = vu + Try(vo(meta.kernelExpr[U](vi(v), u)).withUnit[U]) match + case Success(q) => Right(q) + case Failure(e) => Left(e.getMessage) } object meta: @@ -52,36 +56,36 @@ object meta: import coulomb.conversion.coefficients.{meta => _, *} import coulomb.infra.meta.{*, given} - given ctx_UnitASTToExpr: ToExpr[UnitAST] with - def apply(ast: UnitAST)(using Quotes): Expr[UnitAST] = - ast match - case UnitAST.UnitType(path) => - '{ UnitAST.UnitType(${ Expr(path) }) } - case UnitAST.Mul(l, r) => - '{ UnitAST.Mul(${ Expr(l) }, ${ Expr(r) }) } - case UnitAST.Div(n, d) => - '{ UnitAST.Div(${ Expr(n) }, ${ Expr(d) }) } - case UnitAST.Pow(b, e) => - '{ UnitAST.Pow(${ Expr(b) }, ${ Expr(e) }) } - - inline def kernelExpr[U](v: Rational, u: UnitAST): Rational = + given ctx_RuntimeUnitToExpr: ToExpr[RuntimeUnit] with + def apply(rtu: RuntimeUnit)(using Quotes): Expr[RuntimeUnit] = + rtu match + case RuntimeUnit.UnitType(path) => + '{ RuntimeUnit.UnitType(${ Expr(path) }) } + case RuntimeUnit.Mul(l, r) => + '{ RuntimeUnit.Mul(${ Expr(l) }, ${ Expr(r) }) } + case RuntimeUnit.Div(n, d) => + '{ RuntimeUnit.Div(${ Expr(n) }, ${ Expr(d) }) } + case RuntimeUnit.Pow(b, e) => + '{ RuntimeUnit.Pow(${ Expr(b) }, ${ Expr(e) }) } + + inline def kernelExpr[U](v: Rational, u: RuntimeUnit): Rational = ${ kernelExprMeta[U]('v, 'u) } - def kernelExprMeta[U](v: Expr[Rational], u: Expr[UnitAST])(using + def kernelExprMeta[U](v: Expr[Rational], u: Expr[RuntimeUnit])(using Quotes, Type[U] ): Expr[Rational] = import quotes.reflect.* val cmp = stagingCompiler - val astU = typeReprAST(TypeRepr.of[U]) - '{ kernelRuntime($v, $u, ${ Expr(astU) })(using $cmp) } + val rtuU = typeReprRTU(TypeRepr.of[U]) + '{ kernelRuntime($v, $u, ${ Expr(rtuU) })(using $cmp) } - def kernelRuntime(v: Rational, astF: UnitAST, astT: UnitAST)(using + def kernelRuntime(v: Rational, rtuF: RuntimeUnit, rtuT: RuntimeUnit)(using staging.Compiler ): Rational = staging.run { import quotes.reflect.* - (astTypeRepr(astF).asType, astTypeRepr(astT).asType) match + (rtuTypeRepr(rtuF).asType, rtuTypeRepr(rtuT).asType) match case ('[uf], '[ut]) => '{ ${ Expr(v) } * coefficientRational[uf, ut] } } @@ -96,49 +100,49 @@ object meta: // I'm not even sorry. null.asInstanceOf[Expr[staging.Compiler]] - def unitAST[U](using Quotes, Type[U]): Expr[UnitAST] = + def unitRTU[U](using Quotes, Type[U]): Expr[RuntimeUnit] = import quotes.reflect.* - Expr(typeReprAST(TypeRepr.of[U])) + Expr(typeReprRTU(TypeRepr.of[U])) - def astTypeRepr(using Quotes)( - ast: UnitAST + def rtuTypeRepr(using Quotes)( + rtu: RuntimeUnit ): quotes.reflect.TypeRepr = import quotes.reflect.* - ast match - case UnitAST.UnitType(path) => fqTypeRepr(path) - case UnitAST.Mul(l, r) => - val ltr = astTypeRepr(l) - val rtr = astTypeRepr(r) + rtu match + case RuntimeUnit.UnitType(path) => fqTypeRepr(path) + case RuntimeUnit.Mul(l, r) => + val ltr = rtuTypeRepr(l) + val rtr = rtuTypeRepr(r) TypeRepr.of[coulomb.`*`].appliedTo(List(ltr, rtr)) - case UnitAST.Div(n, d) => - val ntr = astTypeRepr(n) - val dtr = astTypeRepr(d) + case RuntimeUnit.Div(n, d) => + val ntr = rtuTypeRepr(n) + val dtr = rtuTypeRepr(d) TypeRepr.of[coulomb.`/`].appliedTo(List(ntr, dtr)) - case UnitAST.Pow(b, e) => - val btr = astTypeRepr(b) + case RuntimeUnit.Pow(b, e) => + val btr = rtuTypeRepr(b) val etr = rationalTE(e) TypeRepr.of[coulomb.`^`].appliedTo(List(btr, etr)) - def typeReprAST(using Quotes)( + def typeReprRTU(using Quotes)( tr: quotes.reflect.TypeRepr - ): UnitAST = + ): RuntimeUnit = import quotes.reflect.* tr match case AppliedType(op, List(lu, ru)) if (op =:= TypeRepr.of[coulomb.`*`]) => - UnitAST.Mul(typeReprAST(lu), typeReprAST(ru)) + RuntimeUnit.Mul(typeReprRTU(lu), typeReprRTU(ru)) case AppliedType(op, List(lu, ru)) if (op =:= TypeRepr.of[coulomb.`/`]) => - UnitAST.Div(typeReprAST(lu), typeReprAST(ru)) + RuntimeUnit.Div(typeReprRTU(lu), typeReprRTU(ru)) case AppliedType(op, List(b, e)) if (op =:= TypeRepr.of[coulomb.`^`]) => val rationalTE(ev) = e: @unchecked - UnitAST.Pow(typeReprAST(b), ev) + RuntimeUnit.Pow(typeReprRTU(b), ev) case t => // should add checking for types with type-args here - // possibly an explicit non dealiasting policy here would allow + // possibly an explicit non dealirtuing policy here would allow // parameterized types to be handled via typedef aliases? - UnitAST.UnitType(t.typeSymbol.fullName) + RuntimeUnit.UnitType(t.typeSymbol.fullName) def fqTypeRepr(using Quotes)(path: String): quotes.reflect.TypeRepr = fqTypeRepr(path.split('.').toIndexedSeq) From c067964d9e113d71a4c81b157993ca0e93ac05e4 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Wed, 1 Feb 2023 07:21:11 -0700 Subject: [PATCH 021/141] add some annotation ideas --- runtime/src/main/scala/coulomb/runtime/runtime.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/runtime/src/main/scala/coulomb/runtime/runtime.scala b/runtime/src/main/scala/coulomb/runtime/runtime.scala index 8380fd4ef..a2d086745 100644 --- a/runtime/src/main/scala/coulomb/runtime/runtime.scala +++ b/runtime/src/main/scala/coulomb/runtime/runtime.scala @@ -80,6 +80,8 @@ object meta: val rtuU = typeReprRTU(TypeRepr.of[U]) '{ kernelRuntime($v, $u, ${ Expr(rtuU) })(using $cmp) } + // maybe this should return Either[String, Rational] + // this is the core of a hypothetical RuntimeUnitConversion type def kernelRuntime(v: Rational, rtuF: RuntimeUnit, rtuT: RuntimeUnit)(using staging.Compiler ): Rational = From 91f9257768a926b92d02ba8b77aacce36203bf06 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Wed, 1 Feb 2023 12:42:01 -0700 Subject: [PATCH 022/141] RuntimeQuantity --- runtime/src/main/scala/coulomb/runtime/runtime.scala | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/runtime/src/main/scala/coulomb/runtime/runtime.scala b/runtime/src/main/scala/coulomb/runtime/runtime.scala index a2d086745..dcbfaade5 100644 --- a/runtime/src/main/scala/coulomb/runtime/runtime.scala +++ b/runtime/src/main/scala/coulomb/runtime/runtime.scala @@ -34,6 +34,12 @@ object RuntimeUnit: case class Pow(b: RuntimeUnit, e: Rational) extends RuntimeUnit inline def of[U]: RuntimeUnit = ${ meta.unitRTU[U] } +case class RuntimeQuantity[V](value: V, unit: RuntimeUnit) + +object RuntimeQuantity: + inline def apply[V, U](q: Quantity[V, U]): RuntimeQuantity[V] = + RuntimeQuantity(q.value, RuntimeUnit.of[U]) + package syntax { import scala.util.{Try, Success, Failure} import coulomb.conversion.* From 189e46e38ca2630616c4e56d95b4f60c00679d39 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Wed, 1 Feb 2023 19:29:28 -0700 Subject: [PATCH 023/141] bump version-introduced --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 5eac194b6..56204dc60 100644 --- a/build.sbt +++ b/build.sbt @@ -71,7 +71,7 @@ lazy val runtime = crossProject(JVMPlatform, JSPlatform, NativePlatform) ) // make units "% Test" .settings(commonSettings: _*) .settings( - tlVersionIntroduced := Map("3" -> "0.7.3"), + tlVersionIntroduced := Map("3" -> "0.7.4"), libraryDependencies += "org.scala-lang" %% "scala3-staging" % scalaVersion.value ) From 62741339e0f4dc7f41c7ae92272f2eb60b3f2be4 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Fri, 3 Feb 2023 15:37:13 -0700 Subject: [PATCH 024/141] align RuntimeQuantity with Quantity --- .../main/scala/coulomb/runtime/runtime.scala | 101 ++++++++++++------ 1 file changed, 67 insertions(+), 34 deletions(-) diff --git a/runtime/src/main/scala/coulomb/runtime/runtime.scala b/runtime/src/main/scala/coulomb/runtime/runtime.scala index dcbfaade5..399efbbea 100644 --- a/runtime/src/main/scala/coulomb/runtime/runtime.scala +++ b/runtime/src/main/scala/coulomb/runtime/runtime.scala @@ -17,10 +17,12 @@ package coulomb.runtime import scala.quoted.* +import scala.util.{Try, Success, Failure} -import coulomb.* +import coulomb.{infra => _, *} import coulomb.syntax.* import coulomb.rational.Rational +import coulomb.conversion.* sealed abstract class RuntimeUnit: def *(rhs: RuntimeUnit): RuntimeUnit.Mul = RuntimeUnit.Mul(this, rhs) @@ -32,7 +34,14 @@ object RuntimeUnit: case class Mul(lhs: RuntimeUnit, rhs: RuntimeUnit) extends RuntimeUnit case class Div(num: RuntimeUnit, den: RuntimeUnit) extends RuntimeUnit case class Pow(b: RuntimeUnit, e: Rational) extends RuntimeUnit - inline def of[U]: RuntimeUnit = ${ meta.unitRTU[U] } + inline def of[U]: RuntimeUnit = ${ infra.meta.unitRTU[U] } + +def coefficient[V](uf: RuntimeUnit, ut: RuntimeUnit)(using + scmp: staging.Compiler, + vc: ValueConversion[Rational, V] +): Either[String, V] = + import coulomb.runtime.conversion.coefficients.coefficientRational + coefficientRational(uf, ut).map(vc) case class RuntimeQuantity[V](value: V, unit: RuntimeUnit) @@ -40,26 +49,60 @@ object RuntimeQuantity: inline def apply[V, U](q: Quantity[V, U]): RuntimeQuantity[V] = RuntimeQuantity(q.value, RuntimeUnit.of[U]) +extension [VL](ql: RuntimeQuantity[VL]) + inline def toQuantity[VR, UR](using + vi: ValueConversion[VL, Rational], + vo: ValueConversion[Rational, VR] + ): Either[String, Quantity[VR, UR]] = + infra.meta.coefExpr[UR](ql.unit).map { coef => + vo(coef * vi(ql.value)).withUnit[UR] + } + package syntax { - import scala.util.{Try, Success, Failure} - import coulomb.conversion.* - - extension [V](vu: (V, RuntimeUnit)) - inline def toQuantity[U](using - vi: ValueConversion[V, Rational], - vo: ValueConversion[Rational, V] - ): Either[String, Quantity[V, U]] = - val (v, u) = vu - Try(vo(meta.kernelExpr[U](vi(v), u)).withUnit[U]) match - case Success(q) => Right(q) - case Failure(e) => Left(e.getMessage) + extension [V](v: V) + inline def withRuntimeUnit(u: RuntimeUnit): RuntimeQuantity[V] = + RuntimeQuantity(v, u) +} + +package conversion { +abstract class RuntimeUnitConversion[V] extends (V => V) +object RuntimeUnitConversion: + def apply[V](rtuF: RuntimeUnit, rtuT: RuntimeUnit)(using + scmp: staging.Compiler, + vi: ValueConversion[V, Rational], + vo: ValueConversion[Rational, V] + ): Either[String, RuntimeUnitConversion[V]] = + coefficients.coefficientRational(rtuF, rtuT).map { coef => + new RuntimeUnitConversion[V] { + def apply(v: V): V = vo(coef * vi(v)) + } + } + +object coefficients: + import coulomb.runtime.infra.meta.* + import coulomb.conversion.coefficients.{coefficientRational => staticCR} + + def coefficientRational(uf: RuntimeUnit, ut: RuntimeUnit)(using + staging.Compiler + ): Either[String, Rational] = + Try { + staging.run { + import quotes.reflect.* + (rtuTypeRepr(uf).asType, rtuTypeRepr(ut).asType) match + case ('[f], '[t]) => + '{ staticCR[f, t] } + } + } match + case Success(coef) => Right(coef) + case Failure(e) => Left(e.getMessage) } +package infra { + object meta: import scala.unchecked import scala.language.implicitConversions - import coulomb.conversion.coefficients.{meta => _, *} import coulomb.infra.meta.{*, given} given ctx_RuntimeUnitToExpr: ToExpr[RuntimeUnit] with @@ -74,29 +117,18 @@ object meta: case RuntimeUnit.Pow(b, e) => '{ RuntimeUnit.Pow(${ Expr(b) }, ${ Expr(e) }) } - inline def kernelExpr[U](v: Rational, u: RuntimeUnit): Rational = - ${ kernelExprMeta[U]('v, 'u) } + inline def coefExpr[UT](uf: RuntimeUnit): Either[String, Rational] = + ${ coefExprMeta[UT]('uf) } - def kernelExprMeta[U](v: Expr[Rational], u: Expr[RuntimeUnit])(using + def coefExprMeta[UT](uf: Expr[RuntimeUnit])(using Quotes, - Type[U] - ): Expr[Rational] = + Type[UT] + ): Expr[Either[String, Rational]] = import quotes.reflect.* + import coulomb.runtime.conversion.coefficients.coefficientRational val cmp = stagingCompiler - val rtuU = typeReprRTU(TypeRepr.of[U]) - '{ kernelRuntime($v, $u, ${ Expr(rtuU) })(using $cmp) } - - // maybe this should return Either[String, Rational] - // this is the core of a hypothetical RuntimeUnitConversion type - def kernelRuntime(v: Rational, rtuF: RuntimeUnit, rtuT: RuntimeUnit)(using - staging.Compiler - ): Rational = - staging.run { - import quotes.reflect.* - (rtuTypeRepr(rtuF).asType, rtuTypeRepr(rtuT).asType) match - case ('[uf], '[ut]) => - '{ ${ Expr(v) } * coefficientRational[uf, ut] } - } + val ut = typeReprRTU(TypeRepr.of[UT]) + '{ coefficientRational($uf, ${ Expr(ut) })(using $cmp) } def stagingCompiler(using Quotes): Expr[staging.Compiler] = import quotes.reflect.* @@ -211,3 +243,4 @@ object meta: work(defn.RootPackage, path.tail) else work(defn.RootPackage, path) +} From 6ea4503403822f36932e8385787ea52e9a91d14b Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Fri, 3 Feb 2023 15:47:14 -0700 Subject: [PATCH 025/141] given staging.Compiler from above --- .../main/scala/coulomb/runtime/runtime.scala | 324 +++++++++--------- 1 file changed, 160 insertions(+), 164 deletions(-) diff --git a/runtime/src/main/scala/coulomb/runtime/runtime.scala b/runtime/src/main/scala/coulomb/runtime/runtime.scala index 399efbbea..95bf2ec33 100644 --- a/runtime/src/main/scala/coulomb/runtime/runtime.scala +++ b/runtime/src/main/scala/coulomb/runtime/runtime.scala @@ -51,6 +51,7 @@ object RuntimeQuantity: extension [VL](ql: RuntimeQuantity[VL]) inline def toQuantity[VR, UR](using + scmp: staging.Compiler, vi: ValueConversion[VL, Rational], vo: ValueConversion[Rational, VR] ): Either[String, Quantity[VR, UR]] = @@ -65,182 +66,177 @@ package syntax { } package conversion { -abstract class RuntimeUnitConversion[V] extends (V => V) -object RuntimeUnitConversion: - def apply[V](rtuF: RuntimeUnit, rtuT: RuntimeUnit)(using - scmp: staging.Compiler, - vi: ValueConversion[V, Rational], - vo: ValueConversion[Rational, V] - ): Either[String, RuntimeUnitConversion[V]] = - coefficients.coefficientRational(rtuF, rtuT).map { coef => - new RuntimeUnitConversion[V] { - def apply(v: V): V = vo(coef * vi(v)) + abstract class RuntimeUnitConversion[V] extends (V => V) + object RuntimeUnitConversion: + def apply[V](uf: RuntimeUnit, ut: RuntimeUnit)(using + scmp: staging.Compiler, + vi: ValueConversion[V, Rational], + vo: ValueConversion[Rational, V] + ): Either[String, RuntimeUnitConversion[V]] = + coefficients.coefficientRational(uf, ut).map { coef => + new RuntimeUnitConversion[V] { + def apply(v: V): V = vo(coef * vi(v)) + } } - } -object coefficients: - import coulomb.runtime.infra.meta.* - import coulomb.conversion.coefficients.{coefficientRational => staticCR} - - def coefficientRational(uf: RuntimeUnit, ut: RuntimeUnit)(using - staging.Compiler - ): Either[String, Rational] = - Try { - staging.run { - import quotes.reflect.* - (rtuTypeRepr(uf).asType, rtuTypeRepr(ut).asType) match - case ('[f], '[t]) => - '{ staticCR[f, t] } - } - } match - case Success(coef) => Right(coef) - case Failure(e) => Left(e.getMessage) + object coefficients: + import coulomb.runtime.infra.meta.* + import coulomb.conversion.coefficients.{coefficientRational => staticCR} + + def coefficientRational(uf: RuntimeUnit, ut: RuntimeUnit)(using + staging.Compiler + ): Either[String, Rational] = + Try { + staging.run { + import quotes.reflect.* + (rtuTypeRepr(uf).asType, rtuTypeRepr(ut).asType) match + case ('[f], '[t]) => '{ staticCR[f, t] } + } + } match + case Success(coef) => Right(coef) + case Failure(e) => Left(e.getMessage) } package infra { -object meta: - import scala.unchecked - import scala.language.implicitConversions - - import coulomb.infra.meta.{*, given} - - given ctx_RuntimeUnitToExpr: ToExpr[RuntimeUnit] with - def apply(rtu: RuntimeUnit)(using Quotes): Expr[RuntimeUnit] = + object meta: + import scala.unchecked + import scala.language.implicitConversions + + import coulomb.infra.meta.{*, given} + + given ctx_RuntimeUnitToExpr: ToExpr[RuntimeUnit] with + def apply(rtu: RuntimeUnit)(using Quotes): Expr[RuntimeUnit] = + rtu match + case RuntimeUnit.UnitType(path) => + '{ RuntimeUnit.UnitType(${ Expr(path) }) } + case RuntimeUnit.Mul(l, r) => + '{ RuntimeUnit.Mul(${ Expr(l) }, ${ Expr(r) }) } + case RuntimeUnit.Div(n, d) => + '{ RuntimeUnit.Div(${ Expr(n) }, ${ Expr(d) }) } + case RuntimeUnit.Pow(b, e) => + '{ RuntimeUnit.Pow(${ Expr(b) }, ${ Expr(e) }) } + + inline def coefExpr[UT](uf: RuntimeUnit)(using + scmp: staging.Compiler + ): Either[String, Rational] = + ${ coefExprMeta[UT]('uf, 'scmp) } + + def coefExprMeta[UT]( + uf: Expr[RuntimeUnit], + scmp: Expr[staging.Compiler] + )(using + Quotes, + Type[UT] + ): Expr[Either[String, Rational]] = + import quotes.reflect.* + import coulomb.runtime.conversion.coefficients.coefficientRational + val ut = typeReprRTU(TypeRepr.of[UT]) + '{ coefficientRational($uf, ${ Expr(ut) })(using $scmp) } + + def unitRTU[U](using Quotes, Type[U]): Expr[RuntimeUnit] = + import quotes.reflect.* + Expr(typeReprRTU(TypeRepr.of[U])) + + def rtuTypeRepr(using Quotes)( + rtu: RuntimeUnit + ): quotes.reflect.TypeRepr = + import quotes.reflect.* rtu match - case RuntimeUnit.UnitType(path) => - '{ RuntimeUnit.UnitType(${ Expr(path) }) } + case RuntimeUnit.UnitType(path) => fqTypeRepr(path) case RuntimeUnit.Mul(l, r) => - '{ RuntimeUnit.Mul(${ Expr(l) }, ${ Expr(r) }) } + val ltr = rtuTypeRepr(l) + val rtr = rtuTypeRepr(r) + TypeRepr.of[coulomb.`*`].appliedTo(List(ltr, rtr)) case RuntimeUnit.Div(n, d) => - '{ RuntimeUnit.Div(${ Expr(n) }, ${ Expr(d) }) } + val ntr = rtuTypeRepr(n) + val dtr = rtuTypeRepr(d) + TypeRepr.of[coulomb.`/`].appliedTo(List(ntr, dtr)) case RuntimeUnit.Pow(b, e) => - '{ RuntimeUnit.Pow(${ Expr(b) }, ${ Expr(e) }) } - - inline def coefExpr[UT](uf: RuntimeUnit): Either[String, Rational] = - ${ coefExprMeta[UT]('uf) } - - def coefExprMeta[UT](uf: Expr[RuntimeUnit])(using - Quotes, - Type[UT] - ): Expr[Either[String, Rational]] = - import quotes.reflect.* - import coulomb.runtime.conversion.coefficients.coefficientRational - val cmp = stagingCompiler - val ut = typeReprRTU(TypeRepr.of[UT]) - '{ coefficientRational($uf, ${ Expr(ut) })(using $cmp) } - - def stagingCompiler(using Quotes): Expr[staging.Compiler] = - import quotes.reflect.* - Implicits.search(TypeRepr.of[staging.Compiler]) match - case iss: ImplicitSearchSuccess => - iss.tree.asExprOf[staging.Compiler] - case _ => - report.errorAndAbort("no 'given' staging.Compiler is in scope") - // I'm not even sorry. - null.asInstanceOf[Expr[staging.Compiler]] - - def unitRTU[U](using Quotes, Type[U]): Expr[RuntimeUnit] = - import quotes.reflect.* - Expr(typeReprRTU(TypeRepr.of[U])) - - def rtuTypeRepr(using Quotes)( - rtu: RuntimeUnit - ): quotes.reflect.TypeRepr = - import quotes.reflect.* - rtu match - case RuntimeUnit.UnitType(path) => fqTypeRepr(path) - case RuntimeUnit.Mul(l, r) => - val ltr = rtuTypeRepr(l) - val rtr = rtuTypeRepr(r) - TypeRepr.of[coulomb.`*`].appliedTo(List(ltr, rtr)) - case RuntimeUnit.Div(n, d) => - val ntr = rtuTypeRepr(n) - val dtr = rtuTypeRepr(d) - TypeRepr.of[coulomb.`/`].appliedTo(List(ntr, dtr)) - case RuntimeUnit.Pow(b, e) => - val btr = rtuTypeRepr(b) - val etr = rationalTE(e) - TypeRepr.of[coulomb.`^`].appliedTo(List(btr, etr)) - - def typeReprRTU(using Quotes)( - tr: quotes.reflect.TypeRepr - ): RuntimeUnit = - import quotes.reflect.* - tr match - case AppliedType(op, List(lu, ru)) - if (op =:= TypeRepr.of[coulomb.`*`]) => - RuntimeUnit.Mul(typeReprRTU(lu), typeReprRTU(ru)) - case AppliedType(op, List(lu, ru)) - if (op =:= TypeRepr.of[coulomb.`/`]) => - RuntimeUnit.Div(typeReprRTU(lu), typeReprRTU(ru)) - case AppliedType(op, List(b, e)) - if (op =:= TypeRepr.of[coulomb.`^`]) => - val rationalTE(ev) = e: @unchecked - RuntimeUnit.Pow(typeReprRTU(b), ev) - case t => - // should add checking for types with type-args here - // possibly an explicit non dealirtuing policy here would allow - // parameterized types to be handled via typedef aliases? - RuntimeUnit.UnitType(t.typeSymbol.fullName) - - def fqTypeRepr(using Quotes)(path: String): quotes.reflect.TypeRepr = - fqTypeRepr(path.split('.').toIndexedSeq) - - def fqFieldSymbol(using Quotes)(path: String): quotes.reflect.Symbol = - fqFieldSymbol(path.split('.').toIndexedSeq) - - def fqTypeRepr(using Quotes)( - path: Seq[String] - ): quotes.reflect.TypeRepr = - import quotes.reflect.* - if (path.isEmpty) - report.errorAndAbort("fqTypeRepr: empty path") - TypeRepr.of[Unit] - else - val q = fqFieldSymbol(path.dropRight(1)) - val qt = q.typeMembers.filter(_.name == path.last) - if (qt.length == 1) qt.head.typeRef - else - report.errorAndAbort( - s"""fqTypeRepr: bad path ${path.mkString( - "." - )} at ${path.last}""" - ) + val btr = rtuTypeRepr(b) + val etr = rationalTE(e) + TypeRepr.of[coulomb.`^`].appliedTo(List(btr, etr)) + + def typeReprRTU(using Quotes)( + tr: quotes.reflect.TypeRepr + ): RuntimeUnit = + import quotes.reflect.* + tr match + case AppliedType(op, List(lu, ru)) + if (op =:= TypeRepr.of[coulomb.`*`]) => + RuntimeUnit.Mul(typeReprRTU(lu), typeReprRTU(ru)) + case AppliedType(op, List(lu, ru)) + if (op =:= TypeRepr.of[coulomb.`/`]) => + RuntimeUnit.Div(typeReprRTU(lu), typeReprRTU(ru)) + case AppliedType(op, List(b, e)) + if (op =:= TypeRepr.of[coulomb.`^`]) => + val rationalTE(ev) = e: @unchecked + RuntimeUnit.Pow(typeReprRTU(b), ev) + case t => + // should add checking for types with type-args here + // possibly an explicit non dealirtuing policy here would allow + // parameterized types to be handled via typedef aliases? + RuntimeUnit.UnitType(t.typeSymbol.fullName) + + def fqTypeRepr(using Quotes)(path: String): quotes.reflect.TypeRepr = + fqTypeRepr(path.split('.').toIndexedSeq) + + def fqFieldSymbol(using Quotes)(path: String): quotes.reflect.Symbol = + fqFieldSymbol(path.split('.').toIndexedSeq) + + def fqTypeRepr(using Quotes)( + path: Seq[String] + ): quotes.reflect.TypeRepr = + import quotes.reflect.* + if (path.isEmpty) + report.errorAndAbort("fqTypeRepr: empty path") TypeRepr.of[Unit] - - def fqFieldSymbol(using Quotes)( - path: Seq[String] - ): quotes.reflect.Symbol = - import quotes.reflect.* - def work(q: Symbol, tail: Seq[String]): Symbol = - if (tail.isEmpty) q else - // look for modules first - // this includes packages and objects - val qt = q.declarations.filter { x => - (x.name == tail.head) && x.flags.is(Flags.Module) - } - // there are sometimes two symbols representing a module - // is the difference important to this function? - if (qt.length > 0) work(qt.head, tail.tail) + val q = fqFieldSymbol(path.dropRight(1)) + val qt = q.typeMembers.filter(_.name == path.last) + if (qt.length == 1) qt.head.typeRef + else + report.errorAndAbort( + s"""fqTypeRepr: bad path ${path.mkString( + "." + )} at ${path.last}""" + ) + TypeRepr.of[Unit] + + def fqFieldSymbol(using Quotes)( + path: Seq[String] + ): quotes.reflect.Symbol = + import quotes.reflect.* + def work(q: Symbol, tail: Seq[String]): Symbol = + if (tail.isEmpty) q else - // if no module exists, look for declared symbol - val f = q.declarations.filter { x => x.name == tail.head } - // expect a unique field declaration in this case - if ((f.length == 1) && (tail.length == 1)) f.head + // look for modules first + // this includes packages and objects + val qt = q.declarations.filter { x => + (x.name == tail.head) && x.flags.is(Flags.Module) + } + // there are sometimes two symbols representing a module + // is the difference important to this function? + if (qt.length > 0) work(qt.head, tail.tail) else - // if we cannot find a field here, it is a failure - report.errorAndAbort( - s"""fqFieldSymbol: bad path ${path.mkString( - "." - )} at ${tail.head}""" - ) - defn.RootPackage - if (path.isEmpty) - defn.RootPackage - else if (path.head == "_root_") - work(defn.RootPackage, path.tail) - else - work(defn.RootPackage, path) + // if no module exists, look for declared symbol + val f = q.declarations.filter { x => + x.name == tail.head + } + // expect a unique field declaration in this case + if ((f.length == 1) && (tail.length == 1)) f.head + else + // if we cannot find a field here, it is a failure + report.errorAndAbort( + s"""fqFieldSymbol: bad path ${path.mkString( + "." + )} at ${tail.head}""" + ) + defn.RootPackage + if (path.isEmpty) + defn.RootPackage + else if (path.head == "_root_") + work(defn.RootPackage, path.tail) + else + work(defn.RootPackage, path) } From 2a963eedb78bca9849dece6e98fca5a11c0c1fa7 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Fri, 3 Feb 2023 16:09:36 -0700 Subject: [PATCH 026/141] RuntimeUnit toString --- runtime/src/main/scala/coulomb/runtime/runtime.scala | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/runtime/src/main/scala/coulomb/runtime/runtime.scala b/runtime/src/main/scala/coulomb/runtime/runtime.scala index 95bf2ec33..b3d27275c 100644 --- a/runtime/src/main/scala/coulomb/runtime/runtime.scala +++ b/runtime/src/main/scala/coulomb/runtime/runtime.scala @@ -28,6 +28,16 @@ sealed abstract class RuntimeUnit: def *(rhs: RuntimeUnit): RuntimeUnit.Mul = RuntimeUnit.Mul(this, rhs) def /(den: RuntimeUnit): RuntimeUnit.Div = RuntimeUnit.Div(this, den) def ^(e: Rational): RuntimeUnit.Pow = RuntimeUnit.Pow(this, e) + override def toString: String = + def paren(s: String, tl: Boolean): String = + if (tl) s else s"($s)" + def work(u: RuntimeUnit, tl: Boolean = false): String = + u match + case RuntimeUnit.UnitType(path) => path.split('.').last + case RuntimeUnit.Mul(l, r) => paren(s"${work(l)}*${work(r)}", tl) + case RuntimeUnit.Div(n, d) => paren(s"${work(n)}/${work(d)}", tl) + case RuntimeUnit.Pow(b, e) => paren(s"${work(b)}^$e", tl) + work(this, tl = true) object RuntimeUnit: case class UnitType(path: String) extends RuntimeUnit From a71dfb4b94d6831c6e9a2012f29b1a7f4eb5a0d3 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Fri, 3 Feb 2023 16:10:55 -0700 Subject: [PATCH 027/141] RuntimeUnit toString --- runtime/src/main/scala/coulomb/runtime/runtime.scala | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/runtime/src/main/scala/coulomb/runtime/runtime.scala b/runtime/src/main/scala/coulomb/runtime/runtime.scala index b3d27275c..9d5d35903 100644 --- a/runtime/src/main/scala/coulomb/runtime/runtime.scala +++ b/runtime/src/main/scala/coulomb/runtime/runtime.scala @@ -33,10 +33,14 @@ sealed abstract class RuntimeUnit: if (tl) s else s"($s)" def work(u: RuntimeUnit, tl: Boolean = false): String = u match - case RuntimeUnit.UnitType(path) => path.split('.').last - case RuntimeUnit.Mul(l, r) => paren(s"${work(l)}*${work(r)}", tl) - case RuntimeUnit.Div(n, d) => paren(s"${work(n)}/${work(d)}", tl) - case RuntimeUnit.Pow(b, e) => paren(s"${work(b)}^$e", tl) + case RuntimeUnit.UnitType(path) => + path.split('.').last + case RuntimeUnit.Mul(l, r) => + paren(s"${work(l)}*${work(r)}", tl) + case RuntimeUnit.Div(n, d) => + paren(s"${work(n)}/${work(d)}", tl) + case RuntimeUnit.Pow(b, e) => + paren(s"${work(b)}^$e", tl) work(this, tl = true) object RuntimeUnit: From 0bea9c7de5a64e796054843edfcf6b514944fdde Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Fri, 3 Feb 2023 16:40:50 -0700 Subject: [PATCH 028/141] units % Test --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 56204dc60..422c3ac6b 100644 --- a/build.sbt +++ b/build.sbt @@ -67,8 +67,8 @@ lazy val runtime = crossProject(JVMPlatform, JSPlatform, NativePlatform) .settings(name := "coulomb-runtime") .dependsOn( core % "compile->compile;test->test", - units - ) // make units "% Test" + units % Test + ) .settings(commonSettings: _*) .settings( tlVersionIntroduced := Map("3" -> "0.7.4"), From f1f596087912aa895e439050585f368021042b0e Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 4 Feb 2023 07:56:08 -0700 Subject: [PATCH 029/141] JVM only. SAD. --- build.sbt | 2 +- runtime/src/test/scala/coulomb/quantity.scala | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 runtime/src/test/scala/coulomb/quantity.scala diff --git a/build.sbt b/build.sbt index 422c3ac6b..fd2591b9d 100644 --- a/build.sbt +++ b/build.sbt @@ -61,7 +61,7 @@ lazy val units = crossProject(JVMPlatform, JSPlatform, NativePlatform) ) // see also: https://github.com/lampepfl/dotty/issues/7647 -lazy val runtime = crossProject(JVMPlatform, JSPlatform, NativePlatform) +lazy val runtime = crossProject(JVMPlatform /*, JSPlatform, NativePlatform */) .crossType(CrossType.Pure) .in(file("runtime")) .settings(name := "coulomb-runtime") diff --git a/runtime/src/test/scala/coulomb/quantity.scala b/runtime/src/test/scala/coulomb/quantity.scala new file mode 100644 index 000000000..2f7b8c5d0 --- /dev/null +++ b/runtime/src/test/scala/coulomb/quantity.scala @@ -0,0 +1,39 @@ +/* + * Copyright 2023 Erik Erlandson + * + * 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. + */ + +import coulomb.testing.CoulombSuite + +class RuntimeQuantitySuite extends CoulombSuite: + import scala.quoted.staging + + import coulomb.* + import coulomb.syntax.* + import coulomb.runtime.* + + import algebra.instances.all.given + import coulomb.ops.algebra.all.{*, given} + + import coulomb.units.si.{*, given} + import coulomb.units.si.prefixes.{*, given} + import coulomb.units.us.{*, given} + + given staging.Compiler = staging.Compiler.make(classOf[staging.Compiler].getClassLoader) + + test("coefficient") { + import coulomb.policy.strict.given + val coef = runtime.coefficient[Double](RuntimeUnit.of[Kilo * Meter], RuntimeUnit.of[Meter]) + assert(coef.contains(1000d)) + } From 1e383ff679d9f9a541d9ec22a261f5c77b58fdfa Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 4 Feb 2023 08:06:47 -0700 Subject: [PATCH 030/141] are you kidding me? --- runtime/src/test/scala/coulomb/quantity.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/src/test/scala/coulomb/quantity.scala b/runtime/src/test/scala/coulomb/quantity.scala index 2f7b8c5d0..4c6032a59 100644 --- a/runtime/src/test/scala/coulomb/quantity.scala +++ b/runtime/src/test/scala/coulomb/quantity.scala @@ -1,5 +1,5 @@ /* - * Copyright 2023 Erik Erlandson + * Copyright 2022 Erik Erlandson * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From cb183147f0ef804ba7acf247131b4eda4f6c097a Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 4 Feb 2023 08:10:39 -0700 Subject: [PATCH 031/141] formatting troll --- runtime/src/test/scala/coulomb/quantity.scala | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/runtime/src/test/scala/coulomb/quantity.scala b/runtime/src/test/scala/coulomb/quantity.scala index 4c6032a59..f729ab55c 100644 --- a/runtime/src/test/scala/coulomb/quantity.scala +++ b/runtime/src/test/scala/coulomb/quantity.scala @@ -30,10 +30,14 @@ class RuntimeQuantitySuite extends CoulombSuite: import coulomb.units.si.prefixes.{*, given} import coulomb.units.us.{*, given} - given staging.Compiler = staging.Compiler.make(classOf[staging.Compiler].getClassLoader) + given staging.Compiler = + staging.Compiler.make(classOf[staging.Compiler].getClassLoader) test("coefficient") { import coulomb.policy.strict.given - val coef = runtime.coefficient[Double](RuntimeUnit.of[Kilo * Meter], RuntimeUnit.of[Meter]) + val coef = runtime.coefficient[Double]( + RuntimeUnit.of[Kilo * Meter], + RuntimeUnit.of[Meter] + ) assert(coef.contains(1000d)) } From 17a3aa4cc87204f04e5b06112b53c1d996562921 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 4 Feb 2023 08:13:46 -0700 Subject: [PATCH 032/141] more formatting troll --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index fd2591b9d..5670bed77 100644 --- a/build.sbt +++ b/build.sbt @@ -61,7 +61,7 @@ lazy val units = crossProject(JVMPlatform, JSPlatform, NativePlatform) ) // see also: https://github.com/lampepfl/dotty/issues/7647 -lazy val runtime = crossProject(JVMPlatform /*, JSPlatform, NativePlatform */) +lazy val runtime = crossProject(JVMPlatform /*, JSPlatform, NativePlatform */ ) .crossType(CrossType.Pure) .in(file("runtime")) .settings(name := "coulomb-runtime") From e26d09496516bf5013a95d978b25d9062e54ba77 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 5 Feb 2023 12:09:17 -0700 Subject: [PATCH 033/141] refactor to CoefficientRuntime --- .../main/scala/coulomb/runtime/runtime.scala | 98 ++++++++++++------- runtime/src/test/scala/coulomb/quantity.scala | 6 +- 2 files changed, 64 insertions(+), 40 deletions(-) diff --git a/runtime/src/main/scala/coulomb/runtime/runtime.scala b/runtime/src/main/scala/coulomb/runtime/runtime.scala index 9d5d35903..650f1a4ad 100644 --- a/runtime/src/main/scala/coulomb/runtime/runtime.scala +++ b/runtime/src/main/scala/coulomb/runtime/runtime.scala @@ -50,13 +50,6 @@ object RuntimeUnit: case class Pow(b: RuntimeUnit, e: Rational) extends RuntimeUnit inline def of[U]: RuntimeUnit = ${ infra.meta.unitRTU[U] } -def coefficient[V](uf: RuntimeUnit, ut: RuntimeUnit)(using - scmp: staging.Compiler, - vc: ValueConversion[Rational, V] -): Either[String, V] = - import coulomb.runtime.conversion.coefficients.coefficientRational - coefficientRational(uf, ut).map(vc) - case class RuntimeQuantity[V](value: V, unit: RuntimeUnit) object RuntimeQuantity: @@ -65,11 +58,11 @@ object RuntimeQuantity: extension [VL](ql: RuntimeQuantity[VL]) inline def toQuantity[VR, UR](using - scmp: staging.Compiler, + crt: CoefficientRuntime, vi: ValueConversion[VL, Rational], vo: ValueConversion[Rational, VR] ): Either[String, Quantity[VR, UR]] = - infra.meta.coefExpr[UR](ql.unit).map { coef => + crt.coefficientRational[UR](ql.unit).map { coef => vo(coef * vi(ql.value)).withUnit[UR] } @@ -79,45 +72,63 @@ package syntax { RuntimeQuantity(v, u) } +def runtimeCoefficient[V](uf: RuntimeUnit, ut: RuntimeUnit)(using + crt: CoefficientRuntime, + vc: ValueConversion[Rational, V] +): Either[String, V] = + crt.coefficient[V](uf, ut) + +abstract class CoefficientRuntime: + def coefficientRational( + uf: RuntimeUnit, + ut: RuntimeUnit + ): Either[String, Rational] + + final inline def coefficientRational[UT]( + uf: RuntimeUnit + ): Either[String, Rational] = + infra.meta.crExpr[UT](this, uf) + + final def coefficient[V](uf: RuntimeUnit, ut: RuntimeUnit)(using + vc: ValueConversion[Rational, V] + ): Either[String, V] = + this.coefficientRational(uf, ut).map(vc) + +object CoefficientRuntime: + // a CoefficientRuntime that leverages a staging compiler to do runtime magic + // it will be possible to define other flavors of CoefficientRuntime that + // do not require staging compiler and so can work with JS and Native + def staging(using scmp: scala.quoted.staging.Compiler): CoefficientRuntime = + new CoefficientRuntime: + def coefficientRational( + uf: RuntimeUnit, + ut: RuntimeUnit + ): Either[String, Rational] = + infra.meta.coefStaging(uf, ut)(using scmp) + package conversion { abstract class RuntimeUnitConversion[V] extends (V => V) + object RuntimeUnitConversion: def apply[V](uf: RuntimeUnit, ut: RuntimeUnit)(using - scmp: staging.Compiler, + crt: CoefficientRuntime, vi: ValueConversion[V, Rational], vo: ValueConversion[Rational, V] ): Either[String, RuntimeUnitConversion[V]] = - coefficients.coefficientRational(uf, ut).map { coef => + crt.coefficientRational(uf, ut).map { coef => new RuntimeUnitConversion[V] { def apply(v: V): V = vo(coef * vi(v)) } } - - object coefficients: - import coulomb.runtime.infra.meta.* - import coulomb.conversion.coefficients.{coefficientRational => staticCR} - - def coefficientRational(uf: RuntimeUnit, ut: RuntimeUnit)(using - staging.Compiler - ): Either[String, Rational] = - Try { - staging.run { - import quotes.reflect.* - (rtuTypeRepr(uf).asType, rtuTypeRepr(ut).asType) match - case ('[f], '[t]) => '{ staticCR[f, t] } - } - } match - case Success(coef) => Right(coef) - case Failure(e) => Left(e.getMessage) } package infra { - object meta: import scala.unchecked import scala.language.implicitConversions import coulomb.infra.meta.{*, given} + import coulomb.conversion.coefficients.coefficientRational given ctx_RuntimeUnitToExpr: ToExpr[RuntimeUnit] with def apply(rtu: RuntimeUnit)(using Quotes): Expr[RuntimeUnit] = @@ -131,22 +142,33 @@ package infra { case RuntimeUnit.Pow(b, e) => '{ RuntimeUnit.Pow(${ Expr(b) }, ${ Expr(e) }) } - inline def coefExpr[UT](uf: RuntimeUnit)(using - scmp: staging.Compiler + def coefStaging(uf: RuntimeUnit, ut: RuntimeUnit)(using + staging.Compiler + ): Either[String, Rational] = + Try { + staging.run { + import quotes.reflect.* + (rtuTypeRepr(uf).asType, rtuTypeRepr(ut).asType) match + case ('[f], '[t]) => '{ coefficientRational[f, t] } + } + } match + case Success(coef) => Right(coef) + case Failure(e) => Left(e.getMessage) + + inline def crExpr[UT]( + cr: CoefficientRuntime, + uf: RuntimeUnit ): Either[String, Rational] = - ${ coefExprMeta[UT]('uf, 'scmp) } + ${ crExprMeta[UT]('cr, 'uf) } - def coefExprMeta[UT]( - uf: Expr[RuntimeUnit], - scmp: Expr[staging.Compiler] - )(using + def crExprMeta[UT](cr: Expr[CoefficientRuntime], uf: Expr[RuntimeUnit])( + using Quotes, Type[UT] ): Expr[Either[String, Rational]] = import quotes.reflect.* - import coulomb.runtime.conversion.coefficients.coefficientRational val ut = typeReprRTU(TypeRepr.of[UT]) - '{ coefficientRational($uf, ${ Expr(ut) })(using $scmp) } + '{ ${ cr }.coefficientRational($uf, ${ Expr(ut) }) } def unitRTU[U](using Quotes, Type[U]): Expr[RuntimeUnit] = import quotes.reflect.* diff --git a/runtime/src/test/scala/coulomb/quantity.scala b/runtime/src/test/scala/coulomb/quantity.scala index f729ab55c..a47dbe35a 100644 --- a/runtime/src/test/scala/coulomb/quantity.scala +++ b/runtime/src/test/scala/coulomb/quantity.scala @@ -33,9 +33,11 @@ class RuntimeQuantitySuite extends CoulombSuite: given staging.Compiler = staging.Compiler.make(classOf[staging.Compiler].getClassLoader) - test("coefficient") { + given CoefficientRuntime = CoefficientRuntime.staging + + test("runtimeCoefficient") { import coulomb.policy.strict.given - val coef = runtime.coefficient[Double]( + val coef = runtimeCoefficient[Double]( RuntimeUnit.of[Kilo * Meter], RuntimeUnit.of[Meter] ) From b35035c0d15b21901cf3865f5df4bd6ce40866e0 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Mon, 6 Feb 2023 15:03:30 -0700 Subject: [PATCH 034/141] StagingCoefficientRuntime class --- .../main/scala/coulomb/runtime/runtime.scala | 35 ++++++++++++------- runtime/src/test/scala/coulomb/quantity.scala | 4 ++- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/runtime/src/main/scala/coulomb/runtime/runtime.scala b/runtime/src/main/scala/coulomb/runtime/runtime.scala index 650f1a4ad..243a47d23 100644 --- a/runtime/src/main/scala/coulomb/runtime/runtime.scala +++ b/runtime/src/main/scala/coulomb/runtime/runtime.scala @@ -94,18 +94,6 @@ abstract class CoefficientRuntime: ): Either[String, V] = this.coefficientRational(uf, ut).map(vc) -object CoefficientRuntime: - // a CoefficientRuntime that leverages a staging compiler to do runtime magic - // it will be possible to define other flavors of CoefficientRuntime that - // do not require staging compiler and so can work with JS and Native - def staging(using scmp: scala.quoted.staging.Compiler): CoefficientRuntime = - new CoefficientRuntime: - def coefficientRational( - uf: RuntimeUnit, - ut: RuntimeUnit - ): Either[String, Rational] = - infra.meta.coefStaging(uf, ut)(using scmp) - package conversion { abstract class RuntimeUnitConversion[V] extends (V => V) @@ -122,6 +110,29 @@ package conversion { } } +package conversion.runtimes { + import scala.quoted.staging + import coulomb.runtime.infra.meta + + // a CoefficientRuntime that leverages a staging compiler to do runtime magic + // it will be possible to define other flavors of CoefficientRuntime that + // do not require staging compiler and so can work with JS and Native + class StagingCoefficientRuntime(using + scmp: staging.Compiler + ) extends CoefficientRuntime: + def coefficientRational( + uf: RuntimeUnit, + ut: RuntimeUnit + ): Either[String, Rational] = + meta.coefStaging(uf, ut)(using scmp) + + object StagingCoefficientRuntime: + def apply()(using + scmp: staging.Compiler + ): StagingCoefficientRuntime = + new StagingCoefficientRuntime(using scmp) +} + package infra { object meta: import scala.unchecked diff --git a/runtime/src/test/scala/coulomb/quantity.scala b/runtime/src/test/scala/coulomb/quantity.scala index a47dbe35a..098e0beac 100644 --- a/runtime/src/test/scala/coulomb/quantity.scala +++ b/runtime/src/test/scala/coulomb/quantity.scala @@ -30,10 +30,12 @@ class RuntimeQuantitySuite extends CoulombSuite: import coulomb.units.si.prefixes.{*, given} import coulomb.units.us.{*, given} + import coulomb.runtime.conversion.runtimes.StagingCoefficientRuntime + given staging.Compiler = staging.Compiler.make(classOf[staging.Compiler].getClassLoader) - given CoefficientRuntime = CoefficientRuntime.staging + given CoefficientRuntime = StagingCoefficientRuntime() test("runtimeCoefficient") { import coulomb.policy.strict.given From df20eaa7145898039b27d4727743880bc5709cc4 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Mon, 6 Feb 2023 16:48:52 -0700 Subject: [PATCH 035/141] stub MappingCoefficientRuntime --- runtime/src/main/scala/coulomb/runtime/runtime.scala | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/runtime/src/main/scala/coulomb/runtime/runtime.scala b/runtime/src/main/scala/coulomb/runtime/runtime.scala index 243a47d23..1e509e450 100644 --- a/runtime/src/main/scala/coulomb/runtime/runtime.scala +++ b/runtime/src/main/scala/coulomb/runtime/runtime.scala @@ -133,6 +133,18 @@ package conversion.runtimes { new StagingCoefficientRuntime(using scmp) } +package conversion.runtimes { + class MappingCoefficientRuntime( + baseUnits: Set[RuntimeUnit.UnitType], + derivedUnits: Map[RuntimeUnit.UnitType, RuntimeUnit] + ) extends CoefficientRuntime: + def coefficientRational( + uf: RuntimeUnit, + ut: RuntimeUnit + ): Either[String, Rational] = + Left("No.") +} + package infra { object meta: import scala.unchecked From 9472c66f106d2080aef7a8fbfa12a7f8fcbd1141 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Tue, 7 Feb 2023 07:49:00 -0700 Subject: [PATCH 036/141] UnitConst --- .../main/scala/coulomb/runtime/runtime.scala | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/runtime/src/main/scala/coulomb/runtime/runtime.scala b/runtime/src/main/scala/coulomb/runtime/runtime.scala index 1e509e450..d0bd134bf 100644 --- a/runtime/src/main/scala/coulomb/runtime/runtime.scala +++ b/runtime/src/main/scala/coulomb/runtime/runtime.scala @@ -33,6 +33,8 @@ sealed abstract class RuntimeUnit: if (tl) s else s"($s)" def work(u: RuntimeUnit, tl: Boolean = false): String = u match + case RuntimeUnit.UnitConst(value) => + s"$value" case RuntimeUnit.UnitType(path) => path.split('.').last case RuntimeUnit.Mul(l, r) => @@ -44,6 +46,7 @@ sealed abstract class RuntimeUnit: work(this, tl = true) object RuntimeUnit: + case class UnitConst(value: Rational) extends RuntimeUnit case class UnitType(path: String) extends RuntimeUnit case class Mul(lhs: RuntimeUnit, rhs: RuntimeUnit) extends RuntimeUnit case class Div(num: RuntimeUnit, den: RuntimeUnit) extends RuntimeUnit @@ -153,11 +156,23 @@ package infra { import coulomb.infra.meta.{*, given} import coulomb.conversion.coefficients.coefficientRational + given ctx_RuntimeUnitConstToExpr: ToExpr[RuntimeUnit.UnitConst] with + def apply(uc: RuntimeUnit.UnitConst)(using + Quotes + ): Expr[RuntimeUnit.UnitConst] = + '{ RuntimeUnit.UnitConst(${ Expr(uc.value) }) } + + given ctx_RuntimeUnitTypeToExpr: ToExpr[RuntimeUnit.UnitType] with + def apply(ut: RuntimeUnit.UnitType)(using + Quotes + ): Expr[RuntimeUnit.UnitType] = + '{ RuntimeUnit.UnitType(${ Expr(ut.path) }) } + given ctx_RuntimeUnitToExpr: ToExpr[RuntimeUnit] with def apply(rtu: RuntimeUnit)(using Quotes): Expr[RuntimeUnit] = rtu match - case RuntimeUnit.UnitType(path) => - '{ RuntimeUnit.UnitType(${ Expr(path) }) } + case uc: RuntimeUnit.UnitConst => Expr(uc) + case ut: RuntimeUnit.UnitType => Expr(ut) case RuntimeUnit.Mul(l, r) => '{ RuntimeUnit.Mul(${ Expr(l) }, ${ Expr(r) }) } case RuntimeUnit.Div(n, d) => @@ -202,7 +217,8 @@ package infra { ): quotes.reflect.TypeRepr = import quotes.reflect.* rtu match - case RuntimeUnit.UnitType(path) => fqTypeRepr(path) + case RuntimeUnit.UnitConst(value) => rationalTE(value) + case RuntimeUnit.UnitType(path) => fqTypeRepr(path) case RuntimeUnit.Mul(l, r) => val ltr = rtuTypeRepr(l) val rtr = rtuTypeRepr(r) @@ -231,6 +247,8 @@ package infra { if (op =:= TypeRepr.of[coulomb.`^`]) => val rationalTE(ev) = e: @unchecked RuntimeUnit.Pow(typeReprRTU(b), ev) + case rationalTE(v) => + RuntimeUnit.UnitConst(v) case t => // should add checking for types with type-args here // possibly an explicit non dealirtuing policy here would allow From f99d32f712fb9f474d25f008d0aecb307f61ea01 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Thu, 9 Feb 2023 19:08:07 -0700 Subject: [PATCH 037/141] Canonical - missing base unit and derived unit awareness --- .../main/scala/coulomb/runtime/runtime.scala | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/runtime/src/main/scala/coulomb/runtime/runtime.scala b/runtime/src/main/scala/coulomb/runtime/runtime.scala index d0bd134bf..d319628e5 100644 --- a/runtime/src/main/scala/coulomb/runtime/runtime.scala +++ b/runtime/src/main/scala/coulomb/runtime/runtime.scala @@ -19,6 +19,8 @@ package coulomb.runtime import scala.quoted.* import scala.util.{Try, Success, Failure} +import scala.collection.immutable.HashMap + import coulomb.{infra => _, *} import coulomb.syntax.* import coulomb.rational.Rational @@ -53,6 +55,50 @@ object RuntimeUnit: case class Pow(b: RuntimeUnit, e: Rational) extends RuntimeUnit inline def of[U]: RuntimeUnit = ${ infra.meta.unitRTU[U] } +case class Canonical(coef: Rational, sig: Map[RuntimeUnit.UnitType, Rational]): + def *(that: Canonical): Canonical = + val Canonical(rcoef, rsig) = that + val s = Canonical + .merge(sig, rsig)(_ + _) + .filter { case (_, e) => e != Rational.const0 } + Canonical(coef * rcoef, s) + + def /(that: Canonical): Canonical = + val Canonical(rcoef, rsig) = that + val rneg = rsig.map { case (u, e) => (u, -e) } + val s = Canonical + .merge(sig, rneg)(_ + _) + .filter { case (_, e) => e != Rational.const0 } + Canonical(coef / rcoef, s) + + def pow(e: Rational): Canonical = + if (e == Rational.const0) Canonical.one + else if (e == Rational.const1) this + else + val s = sig.map { case (u, ue) => (u, ue * e) } + Canonical(coef.pow(e), s) + +object Canonical: + def merge[K, V](m1: Map[K, V], m2: Map[K, V])(f: (V, V) => V): Map[K, V] = + val ki = m1.keySet & m2.keySet + val r1 = m1.filter { case (k, _) => !ki.contains(k) } + val r2 = m2.filter { case (k, _) => !ki.contains(k) } + val ri = ki.map { k => (k, f(m1(k), m2(k))) } + r1.concat(r2).concat(ri) + + private val s1 = HashMap.empty[RuntimeUnit.UnitType, Rational] + private val r1 = Rational.const1 + + val one: Canonical = Canonical(r1, s1) + + def apply(u: RuntimeUnit): Canonical = + u match + case RuntimeUnit.Mul(l, r) => Canonical(l) * Canonical(r) + case RuntimeUnit.Div(n, d) => Canonical(n) / Canonical(d) + case RuntimeUnit.Pow(b, e) => Canonical(b).pow(e) + case RuntimeUnit.UnitConst(c) => Canonical(c, s1) + case u: RuntimeUnit.UnitType => Canonical(r1, HashMap(u -> r1)) + case class RuntimeQuantity[V](value: V, unit: RuntimeUnit) object RuntimeQuantity: From 4d4801dc05fb33a8dc30ead1f96b743be671ae59 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Fri, 10 Feb 2023 14:54:03 -0700 Subject: [PATCH 038/141] factor runtime meta to meta.scala --- .../scala/coulomb/runtime/infra/meta.scala | 191 ++++++++++++++++++ .../main/scala/coulomb/runtime/runtime.scala | 170 ---------------- 2 files changed, 191 insertions(+), 170 deletions(-) create mode 100644 runtime/src/main/scala/coulomb/runtime/infra/meta.scala diff --git a/runtime/src/main/scala/coulomb/runtime/infra/meta.scala b/runtime/src/main/scala/coulomb/runtime/infra/meta.scala new file mode 100644 index 000000000..88f9179b9 --- /dev/null +++ b/runtime/src/main/scala/coulomb/runtime/infra/meta.scala @@ -0,0 +1,191 @@ +/* + * Copyright 2022 Erik Erlandson + * + * 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 coulomb.runtime.infra + +import scala.quoted.* +import scala.util.{Try, Success, Failure} + +import coulomb.runtime.* +import coulomb.rational.Rational + +object meta: + import scala.unchecked + import scala.language.implicitConversions + + import coulomb.infra.meta.{*, given} + import coulomb.conversion.coefficients.coefficientRational + + given ctx_RuntimeUnitConstToExpr: ToExpr[RuntimeUnit.UnitConst] with + def apply(uc: RuntimeUnit.UnitConst)(using + Quotes + ): Expr[RuntimeUnit.UnitConst] = + '{ RuntimeUnit.UnitConst(${ Expr(uc.value) }) } + + given ctx_RuntimeUnitTypeToExpr: ToExpr[RuntimeUnit.UnitType] with + def apply(ut: RuntimeUnit.UnitType)(using + Quotes + ): Expr[RuntimeUnit.UnitType] = + '{ RuntimeUnit.UnitType(${ Expr(ut.path) }) } + + given ctx_RuntimeUnitToExpr: ToExpr[RuntimeUnit] with + def apply(rtu: RuntimeUnit)(using Quotes): Expr[RuntimeUnit] = + rtu match + case uc: RuntimeUnit.UnitConst => Expr(uc) + case ut: RuntimeUnit.UnitType => Expr(ut) + case RuntimeUnit.Mul(l, r) => + '{ RuntimeUnit.Mul(${ Expr(l) }, ${ Expr(r) }) } + case RuntimeUnit.Div(n, d) => + '{ RuntimeUnit.Div(${ Expr(n) }, ${ Expr(d) }) } + case RuntimeUnit.Pow(b, e) => + '{ RuntimeUnit.Pow(${ Expr(b) }, ${ Expr(e) }) } + + def coefStaging(uf: RuntimeUnit, ut: RuntimeUnit)(using + staging.Compiler + ): Either[String, Rational] = + Try { + staging.run { + import quotes.reflect.* + (rtuTypeRepr(uf).asType, rtuTypeRepr(ut).asType) match + case ('[f], '[t]) => '{ coefficientRational[f, t] } + } + } match + case Success(coef) => Right(coef) + case Failure(e) => Left(e.getMessage) + + inline def crExpr[UT]( + cr: CoefficientRuntime, + uf: RuntimeUnit + ): Either[String, Rational] = + ${ crExprMeta[UT]('cr, 'uf) } + + def crExprMeta[UT](cr: Expr[CoefficientRuntime], uf: Expr[RuntimeUnit])( + using + Quotes, + Type[UT] + ): Expr[Either[String, Rational]] = + import quotes.reflect.* + val ut = typeReprRTU(TypeRepr.of[UT]) + '{ ${ cr }.coefficientRational($uf, ${ Expr(ut) }) } + + def unitRTU[U](using Quotes, Type[U]): Expr[RuntimeUnit] = + import quotes.reflect.* + Expr(typeReprRTU(TypeRepr.of[U])) + + def rtuTypeRepr(using Quotes)( + rtu: RuntimeUnit + ): quotes.reflect.TypeRepr = + import quotes.reflect.* + rtu match + case RuntimeUnit.UnitConst(value) => rationalTE(value) + case RuntimeUnit.UnitType(path) => fqTypeRepr(path) + case RuntimeUnit.Mul(l, r) => + val ltr = rtuTypeRepr(l) + val rtr = rtuTypeRepr(r) + TypeRepr.of[coulomb.`*`].appliedTo(List(ltr, rtr)) + case RuntimeUnit.Div(n, d) => + val ntr = rtuTypeRepr(n) + val dtr = rtuTypeRepr(d) + TypeRepr.of[coulomb.`/`].appliedTo(List(ntr, dtr)) + case RuntimeUnit.Pow(b, e) => + val btr = rtuTypeRepr(b) + val etr = rationalTE(e) + TypeRepr.of[coulomb.`^`].appliedTo(List(btr, etr)) + + def typeReprRTU(using Quotes)( + tr: quotes.reflect.TypeRepr + ): RuntimeUnit = + import quotes.reflect.* + tr match + case AppliedType(op, List(lu, ru)) + if (op =:= TypeRepr.of[coulomb.`*`]) => + RuntimeUnit.Mul(typeReprRTU(lu), typeReprRTU(ru)) + case AppliedType(op, List(lu, ru)) + if (op =:= TypeRepr.of[coulomb.`/`]) => + RuntimeUnit.Div(typeReprRTU(lu), typeReprRTU(ru)) + case AppliedType(op, List(b, e)) + if (op =:= TypeRepr.of[coulomb.`^`]) => + val rationalTE(ev) = e: @unchecked + RuntimeUnit.Pow(typeReprRTU(b), ev) + case rationalTE(v) => + RuntimeUnit.UnitConst(v) + case t => + // should add checking for types with type-args here + // possibly an explicit non dealirtuing policy here would allow + // parameterized types to be handled via typedef aliases? + RuntimeUnit.UnitType(t.typeSymbol.fullName) + + def fqTypeRepr(using Quotes)(path: String): quotes.reflect.TypeRepr = + fqTypeRepr(path.split('.').toIndexedSeq) + + def fqFieldSymbol(using Quotes)(path: String): quotes.reflect.Symbol = + fqFieldSymbol(path.split('.').toIndexedSeq) + + def fqTypeRepr(using Quotes)( + path: Seq[String] + ): quotes.reflect.TypeRepr = + import quotes.reflect.* + if (path.isEmpty) + report.errorAndAbort("fqTypeRepr: empty path") + TypeRepr.of[Unit] + else + val q = fqFieldSymbol(path.dropRight(1)) + val qt = q.typeMembers.filter(_.name == path.last) + if (qt.length == 1) qt.head.typeRef + else + report.errorAndAbort( + s"""fqTypeRepr: bad path ${path.mkString( + "." + )} at ${path.last}""" + ) + TypeRepr.of[Unit] + + def fqFieldSymbol(using Quotes)( + path: Seq[String] + ): quotes.reflect.Symbol = + import quotes.reflect.* + def work(q: Symbol, tail: Seq[String]): Symbol = + if (tail.isEmpty) q + else + // look for modules first + // this includes packages and objects + val qt = q.declarations.filter { x => + (x.name == tail.head) && x.flags.is(Flags.Module) + } + // there are sometimes two symbols representing a module + // is the difference important to this function? + if (qt.length > 0) work(qt.head, tail.tail) + else + // if no module exists, look for declared symbol + val f = q.declarations.filter { x => + x.name == tail.head + } + // expect a unique field declaration in this case + if ((f.length == 1) && (tail.length == 1)) f.head + else + // if we cannot find a field here, it is a failure + report.errorAndAbort( + s"""fqFieldSymbol: bad path ${path.mkString( + "." + )} at ${tail.head}""" + ) + defn.RootPackage + if (path.isEmpty) + defn.RootPackage + else if (path.head == "_root_") + work(defn.RootPackage, path.tail) + else + work(defn.RootPackage, path) diff --git a/runtime/src/main/scala/coulomb/runtime/runtime.scala b/runtime/src/main/scala/coulomb/runtime/runtime.scala index d319628e5..297dadac3 100644 --- a/runtime/src/main/scala/coulomb/runtime/runtime.scala +++ b/runtime/src/main/scala/coulomb/runtime/runtime.scala @@ -193,173 +193,3 @@ package conversion.runtimes { ): Either[String, Rational] = Left("No.") } - -package infra { - object meta: - import scala.unchecked - import scala.language.implicitConversions - - import coulomb.infra.meta.{*, given} - import coulomb.conversion.coefficients.coefficientRational - - given ctx_RuntimeUnitConstToExpr: ToExpr[RuntimeUnit.UnitConst] with - def apply(uc: RuntimeUnit.UnitConst)(using - Quotes - ): Expr[RuntimeUnit.UnitConst] = - '{ RuntimeUnit.UnitConst(${ Expr(uc.value) }) } - - given ctx_RuntimeUnitTypeToExpr: ToExpr[RuntimeUnit.UnitType] with - def apply(ut: RuntimeUnit.UnitType)(using - Quotes - ): Expr[RuntimeUnit.UnitType] = - '{ RuntimeUnit.UnitType(${ Expr(ut.path) }) } - - given ctx_RuntimeUnitToExpr: ToExpr[RuntimeUnit] with - def apply(rtu: RuntimeUnit)(using Quotes): Expr[RuntimeUnit] = - rtu match - case uc: RuntimeUnit.UnitConst => Expr(uc) - case ut: RuntimeUnit.UnitType => Expr(ut) - case RuntimeUnit.Mul(l, r) => - '{ RuntimeUnit.Mul(${ Expr(l) }, ${ Expr(r) }) } - case RuntimeUnit.Div(n, d) => - '{ RuntimeUnit.Div(${ Expr(n) }, ${ Expr(d) }) } - case RuntimeUnit.Pow(b, e) => - '{ RuntimeUnit.Pow(${ Expr(b) }, ${ Expr(e) }) } - - def coefStaging(uf: RuntimeUnit, ut: RuntimeUnit)(using - staging.Compiler - ): Either[String, Rational] = - Try { - staging.run { - import quotes.reflect.* - (rtuTypeRepr(uf).asType, rtuTypeRepr(ut).asType) match - case ('[f], '[t]) => '{ coefficientRational[f, t] } - } - } match - case Success(coef) => Right(coef) - case Failure(e) => Left(e.getMessage) - - inline def crExpr[UT]( - cr: CoefficientRuntime, - uf: RuntimeUnit - ): Either[String, Rational] = - ${ crExprMeta[UT]('cr, 'uf) } - - def crExprMeta[UT](cr: Expr[CoefficientRuntime], uf: Expr[RuntimeUnit])( - using - Quotes, - Type[UT] - ): Expr[Either[String, Rational]] = - import quotes.reflect.* - val ut = typeReprRTU(TypeRepr.of[UT]) - '{ ${ cr }.coefficientRational($uf, ${ Expr(ut) }) } - - def unitRTU[U](using Quotes, Type[U]): Expr[RuntimeUnit] = - import quotes.reflect.* - Expr(typeReprRTU(TypeRepr.of[U])) - - def rtuTypeRepr(using Quotes)( - rtu: RuntimeUnit - ): quotes.reflect.TypeRepr = - import quotes.reflect.* - rtu match - case RuntimeUnit.UnitConst(value) => rationalTE(value) - case RuntimeUnit.UnitType(path) => fqTypeRepr(path) - case RuntimeUnit.Mul(l, r) => - val ltr = rtuTypeRepr(l) - val rtr = rtuTypeRepr(r) - TypeRepr.of[coulomb.`*`].appliedTo(List(ltr, rtr)) - case RuntimeUnit.Div(n, d) => - val ntr = rtuTypeRepr(n) - val dtr = rtuTypeRepr(d) - TypeRepr.of[coulomb.`/`].appliedTo(List(ntr, dtr)) - case RuntimeUnit.Pow(b, e) => - val btr = rtuTypeRepr(b) - val etr = rationalTE(e) - TypeRepr.of[coulomb.`^`].appliedTo(List(btr, etr)) - - def typeReprRTU(using Quotes)( - tr: quotes.reflect.TypeRepr - ): RuntimeUnit = - import quotes.reflect.* - tr match - case AppliedType(op, List(lu, ru)) - if (op =:= TypeRepr.of[coulomb.`*`]) => - RuntimeUnit.Mul(typeReprRTU(lu), typeReprRTU(ru)) - case AppliedType(op, List(lu, ru)) - if (op =:= TypeRepr.of[coulomb.`/`]) => - RuntimeUnit.Div(typeReprRTU(lu), typeReprRTU(ru)) - case AppliedType(op, List(b, e)) - if (op =:= TypeRepr.of[coulomb.`^`]) => - val rationalTE(ev) = e: @unchecked - RuntimeUnit.Pow(typeReprRTU(b), ev) - case rationalTE(v) => - RuntimeUnit.UnitConst(v) - case t => - // should add checking for types with type-args here - // possibly an explicit non dealirtuing policy here would allow - // parameterized types to be handled via typedef aliases? - RuntimeUnit.UnitType(t.typeSymbol.fullName) - - def fqTypeRepr(using Quotes)(path: String): quotes.reflect.TypeRepr = - fqTypeRepr(path.split('.').toIndexedSeq) - - def fqFieldSymbol(using Quotes)(path: String): quotes.reflect.Symbol = - fqFieldSymbol(path.split('.').toIndexedSeq) - - def fqTypeRepr(using Quotes)( - path: Seq[String] - ): quotes.reflect.TypeRepr = - import quotes.reflect.* - if (path.isEmpty) - report.errorAndAbort("fqTypeRepr: empty path") - TypeRepr.of[Unit] - else - val q = fqFieldSymbol(path.dropRight(1)) - val qt = q.typeMembers.filter(_.name == path.last) - if (qt.length == 1) qt.head.typeRef - else - report.errorAndAbort( - s"""fqTypeRepr: bad path ${path.mkString( - "." - )} at ${path.last}""" - ) - TypeRepr.of[Unit] - - def fqFieldSymbol(using Quotes)( - path: Seq[String] - ): quotes.reflect.Symbol = - import quotes.reflect.* - def work(q: Symbol, tail: Seq[String]): Symbol = - if (tail.isEmpty) q - else - // look for modules first - // this includes packages and objects - val qt = q.declarations.filter { x => - (x.name == tail.head) && x.flags.is(Flags.Module) - } - // there are sometimes two symbols representing a module - // is the difference important to this function? - if (qt.length > 0) work(qt.head, tail.tail) - else - // if no module exists, look for declared symbol - val f = q.declarations.filter { x => - x.name == tail.head - } - // expect a unique field declaration in this case - if ((f.length == 1) && (tail.length == 1)) f.head - else - // if we cannot find a field here, it is a failure - report.errorAndAbort( - s"""fqFieldSymbol: bad path ${path.mkString( - "." - )} at ${tail.head}""" - ) - defn.RootPackage - if (path.isEmpty) - defn.RootPackage - else if (path.head == "_root_") - work(defn.RootPackage, path.tail) - else - work(defn.RootPackage, path) -} From ca1142531e4d45428e70b7d31b1b478c46094c69 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Fri, 10 Feb 2023 15:37:32 -0700 Subject: [PATCH 039/141] staging.scala --- .../runtime/conversion/runtimes/staging.scala | 41 +++++++++++++++++++ .../main/scala/coulomb/runtime/runtime.scala | 23 ----------- runtime/src/test/scala/coulomb/quantity.scala | 2 +- 3 files changed, 42 insertions(+), 24 deletions(-) create mode 100644 runtime/src/main/scala/coulomb/runtime/conversion/runtimes/staging.scala diff --git a/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/staging.scala b/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/staging.scala new file mode 100644 index 000000000..e53899d15 --- /dev/null +++ b/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/staging.scala @@ -0,0 +1,41 @@ +/* + * Copyright 2022 Erik Erlandson + * + * 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 coulomb.runtime.conversion.runtimes.staging + +import scala.quoted.staging + +import coulomb.runtime.* +import coulomb.runtime.infra.meta +import coulomb.rational.Rational + +// a CoefficientRuntime that leverages a staging compiler to do runtime magic +// it will be possible to define other flavors of CoefficientRuntime that +// do not require staging compiler and so can work with JS and Native +class StagingCoefficientRuntime(using + scmp: staging.Compiler +) extends CoefficientRuntime: + def coefficientRational( + uf: RuntimeUnit, + ut: RuntimeUnit + ): Either[String, Rational] = + meta.coefStaging(uf, ut)(using scmp) + +object StagingCoefficientRuntime: + def apply()(using + scmp: staging.Compiler + ): StagingCoefficientRuntime = + new StagingCoefficientRuntime(using scmp) diff --git a/runtime/src/main/scala/coulomb/runtime/runtime.scala b/runtime/src/main/scala/coulomb/runtime/runtime.scala index 297dadac3..6606d0fb7 100644 --- a/runtime/src/main/scala/coulomb/runtime/runtime.scala +++ b/runtime/src/main/scala/coulomb/runtime/runtime.scala @@ -159,29 +159,6 @@ package conversion { } } -package conversion.runtimes { - import scala.quoted.staging - import coulomb.runtime.infra.meta - - // a CoefficientRuntime that leverages a staging compiler to do runtime magic - // it will be possible to define other flavors of CoefficientRuntime that - // do not require staging compiler and so can work with JS and Native - class StagingCoefficientRuntime(using - scmp: staging.Compiler - ) extends CoefficientRuntime: - def coefficientRational( - uf: RuntimeUnit, - ut: RuntimeUnit - ): Either[String, Rational] = - meta.coefStaging(uf, ut)(using scmp) - - object StagingCoefficientRuntime: - def apply()(using - scmp: staging.Compiler - ): StagingCoefficientRuntime = - new StagingCoefficientRuntime(using scmp) -} - package conversion.runtimes { class MappingCoefficientRuntime( baseUnits: Set[RuntimeUnit.UnitType], diff --git a/runtime/src/test/scala/coulomb/quantity.scala b/runtime/src/test/scala/coulomb/quantity.scala index 098e0beac..0c491e40e 100644 --- a/runtime/src/test/scala/coulomb/quantity.scala +++ b/runtime/src/test/scala/coulomb/quantity.scala @@ -30,7 +30,7 @@ class RuntimeQuantitySuite extends CoulombSuite: import coulomb.units.si.prefixes.{*, given} import coulomb.units.us.{*, given} - import coulomb.runtime.conversion.runtimes.StagingCoefficientRuntime + import coulomb.runtime.conversion.runtimes.staging.StagingCoefficientRuntime given staging.Compiler = staging.Compiler.make(classOf[staging.Compiler].getClassLoader) From 68938c6b2dd8a157764418ae0b009be6fe4db306 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Fri, 10 Feb 2023 15:57:34 -0700 Subject: [PATCH 040/141] mapping.scala --- .../runtime/conversion/runtimes/mapping.scala | 77 +++++++++++++++++++ .../main/scala/coulomb/runtime/runtime.scala | 61 --------------- 2 files changed, 77 insertions(+), 61 deletions(-) create mode 100644 runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala diff --git a/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala b/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala new file mode 100644 index 000000000..141b9fb16 --- /dev/null +++ b/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala @@ -0,0 +1,77 @@ +/* + * Copyright 2022 Erik Erlandson + * + * 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 coulomb.runtime.conversion.runtimes.mapping + +import scala.collection.immutable.HashMap + +import coulomb.runtime.* +import coulomb.runtime.infra.meta +import coulomb.rational.Rational + +class MappingCoefficientRuntime( + baseUnits: Set[RuntimeUnit.UnitType], + derivedUnits: Map[RuntimeUnit.UnitType, RuntimeUnit] +) extends CoefficientRuntime: + def coefficientRational( + uf: RuntimeUnit, + ut: RuntimeUnit + ): Either[String, Rational] = + Left("No.") + +case class Canonical(coef: Rational, sig: Map[RuntimeUnit.UnitType, Rational]): + def *(that: Canonical): Canonical = + val Canonical(rcoef, rsig) = that + val s = Canonical + .merge(sig, rsig)(_ + _) + .filter { case (_, e) => e != Rational.const0 } + Canonical(coef * rcoef, s) + + def /(that: Canonical): Canonical = + val Canonical(rcoef, rsig) = that + val rneg = rsig.map { case (u, e) => (u, -e) } + val s = Canonical + .merge(sig, rneg)(_ + _) + .filter { case (_, e) => e != Rational.const0 } + Canonical(coef / rcoef, s) + + def pow(e: Rational): Canonical = + if (e == Rational.const0) Canonical.one + else if (e == Rational.const1) this + else + val s = sig.map { case (u, ue) => (u, ue * e) } + Canonical(coef.pow(e), s) + +object Canonical: + def merge[K, V](m1: Map[K, V], m2: Map[K, V])(f: (V, V) => V): Map[K, V] = + val ki = m1.keySet & m2.keySet + val r1 = m1.filter { case (k, _) => !ki.contains(k) } + val r2 = m2.filter { case (k, _) => !ki.contains(k) } + val ri = ki.map { k => (k, f(m1(k), m2(k))) } + r1.concat(r2).concat(ri) + + private val s1 = HashMap.empty[RuntimeUnit.UnitType, Rational] + private val r1 = Rational.const1 + + val one: Canonical = Canonical(r1, s1) + + def apply(u: RuntimeUnit): Canonical = + u match + case RuntimeUnit.Mul(l, r) => Canonical(l) * Canonical(r) + case RuntimeUnit.Div(n, d) => Canonical(n) / Canonical(d) + case RuntimeUnit.Pow(b, e) => Canonical(b).pow(e) + case RuntimeUnit.UnitConst(c) => Canonical(c, s1) + case u: RuntimeUnit.UnitType => Canonical(r1, HashMap(u -> r1)) diff --git a/runtime/src/main/scala/coulomb/runtime/runtime.scala b/runtime/src/main/scala/coulomb/runtime/runtime.scala index 6606d0fb7..16182f478 100644 --- a/runtime/src/main/scala/coulomb/runtime/runtime.scala +++ b/runtime/src/main/scala/coulomb/runtime/runtime.scala @@ -16,11 +16,6 @@ package coulomb.runtime -import scala.quoted.* -import scala.util.{Try, Success, Failure} - -import scala.collection.immutable.HashMap - import coulomb.{infra => _, *} import coulomb.syntax.* import coulomb.rational.Rational @@ -55,50 +50,6 @@ object RuntimeUnit: case class Pow(b: RuntimeUnit, e: Rational) extends RuntimeUnit inline def of[U]: RuntimeUnit = ${ infra.meta.unitRTU[U] } -case class Canonical(coef: Rational, sig: Map[RuntimeUnit.UnitType, Rational]): - def *(that: Canonical): Canonical = - val Canonical(rcoef, rsig) = that - val s = Canonical - .merge(sig, rsig)(_ + _) - .filter { case (_, e) => e != Rational.const0 } - Canonical(coef * rcoef, s) - - def /(that: Canonical): Canonical = - val Canonical(rcoef, rsig) = that - val rneg = rsig.map { case (u, e) => (u, -e) } - val s = Canonical - .merge(sig, rneg)(_ + _) - .filter { case (_, e) => e != Rational.const0 } - Canonical(coef / rcoef, s) - - def pow(e: Rational): Canonical = - if (e == Rational.const0) Canonical.one - else if (e == Rational.const1) this - else - val s = sig.map { case (u, ue) => (u, ue * e) } - Canonical(coef.pow(e), s) - -object Canonical: - def merge[K, V](m1: Map[K, V], m2: Map[K, V])(f: (V, V) => V): Map[K, V] = - val ki = m1.keySet & m2.keySet - val r1 = m1.filter { case (k, _) => !ki.contains(k) } - val r2 = m2.filter { case (k, _) => !ki.contains(k) } - val ri = ki.map { k => (k, f(m1(k), m2(k))) } - r1.concat(r2).concat(ri) - - private val s1 = HashMap.empty[RuntimeUnit.UnitType, Rational] - private val r1 = Rational.const1 - - val one: Canonical = Canonical(r1, s1) - - def apply(u: RuntimeUnit): Canonical = - u match - case RuntimeUnit.Mul(l, r) => Canonical(l) * Canonical(r) - case RuntimeUnit.Div(n, d) => Canonical(n) / Canonical(d) - case RuntimeUnit.Pow(b, e) => Canonical(b).pow(e) - case RuntimeUnit.UnitConst(c) => Canonical(c, s1) - case u: RuntimeUnit.UnitType => Canonical(r1, HashMap(u -> r1)) - case class RuntimeQuantity[V](value: V, unit: RuntimeUnit) object RuntimeQuantity: @@ -158,15 +109,3 @@ package conversion { } } } - -package conversion.runtimes { - class MappingCoefficientRuntime( - baseUnits: Set[RuntimeUnit.UnitType], - derivedUnits: Map[RuntimeUnit.UnitType, RuntimeUnit] - ) extends CoefficientRuntime: - def coefficientRational( - uf: RuntimeUnit, - ut: RuntimeUnit - ): Either[String, Rational] = - Left("No.") -} From bc4a139ef529c5fe9a7002f525fbbb271c89cd8b Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Fri, 10 Feb 2023 19:15:04 -0700 Subject: [PATCH 041/141] refactor canonical --- .../runtime/conversion/runtimes/mapping.scala | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala b/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala index 141b9fb16..a2d7409b1 100644 --- a/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala +++ b/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala @@ -19,7 +19,6 @@ package coulomb.runtime.conversion.runtimes.mapping import scala.collection.immutable.HashMap import coulomb.runtime.* -import coulomb.runtime.infra.meta import coulomb.rational.Rational class MappingCoefficientRuntime( @@ -30,7 +29,23 @@ class MappingCoefficientRuntime( uf: RuntimeUnit, ut: RuntimeUnit ): Either[String, Rational] = - Left("No.") + val Canonical(coef, sig) = canonical(ut / uf) + if (sig.isEmpty) Right(coef) else Left("No.") + + def canonical(u: RuntimeUnit): Canonical = + u match + case RuntimeUnit.Mul(l, r) => canonical(l) * canonical(r) + case RuntimeUnit.Div(n, d) => canonical(n) / canonical(d) + case RuntimeUnit.Pow(b, e) => canonical(b).pow(e) + case RuntimeUnit.UnitConst(c) => Canonical(c, Canonical.one.sig) + case u: RuntimeUnit.UnitType => + Canonical(Rational.const1, HashMap(u -> Rational.const1)) + +object MappingCoefficientRuntime: + final type &:[H, T] + final type TNil + + inline def of[UTL]: MappingCoefficientRuntime = ${ meta.ofUTL[UTL] } case class Canonical(coef: Rational, sig: Map[RuntimeUnit.UnitType, Rational]): def *(that: Canonical): Canonical = @@ -63,15 +78,21 @@ object Canonical: val ri = ki.map { k => (k, f(m1(k), m2(k))) } r1.concat(r2).concat(ri) - private val s1 = HashMap.empty[RuntimeUnit.UnitType, Rational] - private val r1 = Rational.const1 + val one: Canonical = Canonical( + Rational.const1, + HashMap.empty[RuntimeUnit.UnitType, Rational] + ) - val one: Canonical = Canonical(r1, s1) +object meta: + import scala.quoted.* + import scala.util.{Try, Success, Failure} + import scala.unchecked + import scala.language.implicitConversions - def apply(u: RuntimeUnit): Canonical = - u match - case RuntimeUnit.Mul(l, r) => Canonical(l) * Canonical(r) - case RuntimeUnit.Div(n, d) => Canonical(n) / Canonical(d) - case RuntimeUnit.Pow(b, e) => Canonical(b).pow(e) - case RuntimeUnit.UnitConst(c) => Canonical(c, s1) - case u: RuntimeUnit.UnitType => Canonical(r1, HashMap(u -> r1)) + import coulomb.infra.meta.{*, given} + import coulomb.runtime.infra.meta.{*, given} + + def ofUTL[UTL](using Quotes, Type[UTL]): Expr[MappingCoefficientRuntime] = + val bu = Set.empty[RuntimeUnit.UnitType] + val du = Map.empty[RuntimeUnit.UnitType, RuntimeUnit] + '{ new MappingCoefficientRuntime(${ Expr(bu) }, ${ Expr(du) }) } From 6ba1cc771a3f11448178312df9c2a8b58572ff6d Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Fri, 10 Feb 2023 19:44:50 -0700 Subject: [PATCH 042/141] clean up signature of canonical to use Either --- .../runtime/conversion/runtimes/mapping.scala | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala b/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala index a2d7409b1..73fac1175 100644 --- a/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala +++ b/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala @@ -29,17 +29,31 @@ class MappingCoefficientRuntime( uf: RuntimeUnit, ut: RuntimeUnit ): Either[String, Rational] = - val Canonical(coef, sig) = canonical(ut / uf) - if (sig.isEmpty) Right(coef) else Left("No.") + canonical(ut / uf).flatMap { qcan => + val Canonical(coef, sig) = qcan + if (sig.isEmpty) Right(coef) else Left("No.") + } - def canonical(u: RuntimeUnit): Canonical = + def canonical(u: RuntimeUnit): Either[String, Canonical] = u match - case RuntimeUnit.Mul(l, r) => canonical(l) * canonical(r) - case RuntimeUnit.Div(n, d) => canonical(n) / canonical(d) - case RuntimeUnit.Pow(b, e) => canonical(b).pow(e) - case RuntimeUnit.UnitConst(c) => Canonical(c, Canonical.one.sig) - case u: RuntimeUnit.UnitType => - Canonical(Rational.const1, HashMap(u -> Rational.const1)) + case RuntimeUnit.Mul(l, r) => canonical(l) * canonical(r) + case RuntimeUnit.Div(n, d) => canonical(n) / canonical(d) + case RuntimeUnit.Pow(b, e) => canonical(b).pow(e) + case RuntimeUnit.UnitConst(c) => + Right(Canonical(c, Canonical.one.sig)) + case u: RuntimeUnit.UnitType if (baseUnits.contains(u)) => + Right(Canonical(Rational.const1, HashMap(u -> Rational.const1))) + case u: RuntimeUnit.UnitType if (derivedUnits.contains(u)) => + canonical(derivedUnits(u)) + case _ => Left(s"canonical: unrecognized unit $u") + +extension (lhs: Either[String, Canonical]) + def *(rhs: Either[String, Canonical]): Either[String, Canonical] = + for { l <- lhs; r <- rhs } yield l * r + def /(rhs: Either[String, Canonical]): Either[String, Canonical] = + for { l <- lhs; r <- rhs } yield l / r + def pow(e: Rational): Either[String, Canonical] = + lhs.map { l => l.pow(e) } object MappingCoefficientRuntime: final type &:[H, T] From 377793715a4a252d75cd5a4fe6d5e7125861a696 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 11 Feb 2023 11:06:26 -0700 Subject: [PATCH 043/141] utlClosure --- core/src/main/scala/coulomb/infra/meta.scala | 25 +++++++++++ .../runtime/conversion/runtimes/mapping.scala | 44 ++++++++++++++++++- .../scala/coulomb/runtime/infra/meta.scala | 15 ++++--- 3 files changed, 77 insertions(+), 7 deletions(-) diff --git a/core/src/main/scala/coulomb/infra/meta.scala b/core/src/main/scala/coulomb/infra/meta.scala index f33ac84fa..c47e48ed9 100644 --- a/core/src/main/scala/coulomb/infra/meta.scala +++ b/core/src/main/scala/coulomb/infra/meta.scala @@ -302,6 +302,31 @@ object meta: Some(cansig(d)) case _ => None + object derivedunitTR: + def unapply(using Quotes)( + u: quotes.reflect.TypeRepr + ): Option[quotes.reflect.TypeRepr] = + import quotes.reflect.* + Implicits.search( + TypeRepr + .of[DerivedUnit] + .appliedTo( + List( + u, + TypeBounds.empty, + TypeBounds.empty, + TypeBounds.empty + ) + ) + ) match + case iss: ImplicitSearchSuccess => + Some( + iss.tree.tpe.baseType( + TypeRepr.of[DerivedUnit].typeSymbol + ) + ) + case _ => None + object deltaunit: def unapply(using Quotes)( u: quotes.reflect.TypeRepr diff --git a/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala b/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala index 73fac1175..6cc7f6b29 100644 --- a/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala +++ b/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala @@ -106,7 +106,47 @@ object meta: import coulomb.infra.meta.{*, given} import coulomb.runtime.infra.meta.{*, given} + import MappingCoefficientRuntime.{TNil, &:} + def ofUTL[UTL](using Quotes, Type[UTL]): Expr[MappingCoefficientRuntime] = - val bu = Set.empty[RuntimeUnit.UnitType] - val du = Map.empty[RuntimeUnit.UnitType, RuntimeUnit] + import quotes.reflect.* + val (bu, du) = utlClosure(TypeRepr.of[UTL]) '{ new MappingCoefficientRuntime(${ Expr(bu) }, ${ Expr(du) }) } + + def utlClosure(using Quotes)( + utl: quotes.reflect.TypeRepr + ): (Set[RuntimeUnit.UnitType], Map[RuntimeUnit.UnitType, RuntimeUnit]) = + import quotes.reflect.* + utl match + case tnil if (tnil =:= TypeRepr.of[TNil]) => + ( + Set.empty[RuntimeUnit.UnitType], + Map.empty[RuntimeUnit.UnitType, RuntimeUnit] + ) + case AppliedType(t, List(head, tail)) if (t =:= TypeRepr.of[&:]) => + val (bu, du) = utlClosure(tail) + head match + case baseunit() => + (bu + typeReprUT(head), du) + case derivedunitTR(dtr) => + val AppliedType(_, List(_, d, _, _)) = dtr: @unchecked + val (dbu, ddu) = utClosure(d) + ( + bu ++ dbu, + (du ++ ddu) + (typeReprUT(head) -> typeReprRTU(d)) + ) + case _ => + val (hbu, hdu) = utClosure(head) + (bu ++ hbu, du ++ hdu) + case _ => + report.errorAndAbort("no") + null.asInstanceOf[Nothing] + + def utClosure(using Quotes)( + utl: quotes.reflect.TypeRepr + ): (Set[RuntimeUnit.UnitType], Map[RuntimeUnit.UnitType, RuntimeUnit]) = + import quotes.reflect.* + ( + Set.empty[RuntimeUnit.UnitType], + Map.empty[RuntimeUnit.UnitType, RuntimeUnit] + ) diff --git a/runtime/src/main/scala/coulomb/runtime/infra/meta.scala b/runtime/src/main/scala/coulomb/runtime/infra/meta.scala index 88f9179b9..5b184dc3d 100644 --- a/runtime/src/main/scala/coulomb/runtime/infra/meta.scala +++ b/runtime/src/main/scala/coulomb/runtime/infra/meta.scala @@ -122,11 +122,16 @@ object meta: RuntimeUnit.Pow(typeReprRTU(b), ev) case rationalTE(v) => RuntimeUnit.UnitConst(v) - case t => - // should add checking for types with type-args here - // possibly an explicit non dealirtuing policy here would allow - // parameterized types to be handled via typedef aliases? - RuntimeUnit.UnitType(t.typeSymbol.fullName) + case ut => typeReprUT(ut) + + def typeReprUT(using Quotes)( + tr: quotes.reflect.TypeRepr + ): RuntimeUnit.UnitType = + import quotes.reflect.* + // should add checking for types with type-args here + // possibly an explicit non dealiasing policy here would allow + // parameterized types to be handled via typedef aliases? + RuntimeUnit.UnitType(tr.typeSymbol.fullName) def fqTypeRepr(using Quotes)(path: String): quotes.reflect.TypeRepr = fqTypeRepr(path.split('.').toIndexedSeq) From 07963658f64bc72c92f0346f4a5c4ec1364dadba Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 11 Feb 2023 11:18:31 -0700 Subject: [PATCH 044/141] refactor derivedunit method --- core/src/main/scala/coulomb/infra/meta.scala | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/core/src/main/scala/coulomb/infra/meta.scala b/core/src/main/scala/coulomb/infra/meta.scala index c47e48ed9..da08b27c0 100644 --- a/core/src/main/scala/coulomb/infra/meta.scala +++ b/core/src/main/scala/coulomb/infra/meta.scala @@ -277,28 +277,15 @@ object meta: u: quotes.reflect.TypeRepr ): Option[(Rational, List[(quotes.reflect.TypeRepr, Rational)])] = import quotes.reflect.* - Implicits.search( - TypeRepr - .of[DerivedUnit] - .appliedTo( - List( - u, - TypeBounds.empty, - TypeBounds.empty, - TypeBounds.empty - ) - ) - ) match - case iss: ImplicitSearchSuccess => + u match + case derivedunitTR(dtr) => mode match case SigMode.Simplify => // don't expand the signature definition in simplify mode Some((Rational.const1, (u, Rational.const1) :: Nil)) case _ => val AppliedType(_, List(_, d, _, _)) = - iss.tree.tpe.baseType( - TypeRepr.of[DerivedUnit].typeSymbol - ): @unchecked + dtr: @unchecked Some(cansig(d)) case _ => None From f8c8f658901adf3adffcae606003086beddb47d9 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 11 Feb 2023 12:51:52 -0700 Subject: [PATCH 045/141] working draft of MappingCoefficientRuntime --- .../runtime/conversion/runtimes/mapping.scala | 67 +++++++++++++------ 1 file changed, 46 insertions(+), 21 deletions(-) diff --git a/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala b/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala index 6cc7f6b29..191c609aa 100644 --- a/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala +++ b/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala @@ -31,7 +31,8 @@ class MappingCoefficientRuntime( ): Either[String, Rational] = canonical(ut / uf).flatMap { qcan => val Canonical(coef, sig) = qcan - if (sig.isEmpty) Right(coef) else Left("No.") + if (sig.isEmpty) Right(coef) + else Left(s"non-convertible units: $uf, $ut") } def canonical(u: RuntimeUnit): Either[String, Canonical] = @@ -118,35 +119,59 @@ object meta: ): (Set[RuntimeUnit.UnitType], Map[RuntimeUnit.UnitType, RuntimeUnit]) = import quotes.reflect.* utl match - case tnil if (tnil =:= TypeRepr.of[TNil]) => - ( - Set.empty[RuntimeUnit.UnitType], - Map.empty[RuntimeUnit.UnitType, RuntimeUnit] - ) + case tnil if (tnil =:= TypeRepr.of[TNil]) => emptyClosure case AppliedType(t, List(head, tail)) if (t =:= TypeRepr.of[&:]) => - val (bu, du) = utlClosure(tail) - head match + val (tbu, tdu) = utlClosure(tail) + val (hbu, hdu) = utClosure(head) + (hbu ++ tbu, hdu ++ tdu) + case _ => + report.errorAndAbort( + s"utlClosure: bad unit type list ${utl.show}" + ) + null.asInstanceOf[Nothing] + + def utClosure(using Quotes)( + tr: quotes.reflect.TypeRepr + ): (Set[RuntimeUnit.UnitType], Map[RuntimeUnit.UnitType, RuntimeUnit]) = + import quotes.reflect.* + tr match + case AppliedType(op, List(lu, ru)) + if (op =:= TypeRepr.of[coulomb.`*`]) => + val (lbu, ldu) = utClosure(lu) + val (rbu, rdu) = utClosure(ru) + (lbu ++ rbu, ldu ++ rdu) + case AppliedType(op, List(lu, ru)) + if (op =:= TypeRepr.of[coulomb.`/`]) => + val (lbu, ldu) = utClosure(lu) + val (rbu, rdu) = utClosure(ru) + (lbu ++ rbu, ldu ++ rdu) + case AppliedType(op, List(b, _)) + if (op =:= TypeRepr.of[coulomb.`^`]) => + utClosure(b) + case rationalTE(v) => + emptyClosure + case ut => + ut match case baseunit() => - (bu + typeReprUT(head), du) + (Set(typeReprUT(ut)), emptyMap) case derivedunitTR(dtr) => val AppliedType(_, List(_, d, _, _)) = dtr: @unchecked val (dbu, ddu) = utClosure(d) ( - bu ++ dbu, - (du ++ ddu) + (typeReprUT(head) -> typeReprRTU(d)) + dbu, + ddu + (typeReprUT(ut) -> typeReprRTU(d)) ) case _ => - val (hbu, hdu) = utClosure(head) - (bu ++ hbu, du ++ hdu) - case _ => - report.errorAndAbort("no") - null.asInstanceOf[Nothing] + report.errorAndAbort( + s"closureUT: bad unit type ${ut.show}" + ) + null.asInstanceOf[Nothing] - def utClosure(using Quotes)( - utl: quotes.reflect.TypeRepr - ): (Set[RuntimeUnit.UnitType], Map[RuntimeUnit.UnitType, RuntimeUnit]) = - import quotes.reflect.* + private val emptyMap = + Map.empty[RuntimeUnit.UnitType, RuntimeUnit] + + private val emptyClosure = ( Set.empty[RuntimeUnit.UnitType], - Map.empty[RuntimeUnit.UnitType, RuntimeUnit] + emptyMap ) From 4cfdaadc1a250df11f7ee63b39eb0b9d82965af9 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 11 Feb 2023 15:12:40 -0700 Subject: [PATCH 046/141] stagingquantity.scala --- .../scala/coulomb/{quantity.scala => stagingquantity.scala} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename runtime/src/test/scala/coulomb/{quantity.scala => stagingquantity.scala} (96%) diff --git a/runtime/src/test/scala/coulomb/quantity.scala b/runtime/src/test/scala/coulomb/stagingquantity.scala similarity index 96% rename from runtime/src/test/scala/coulomb/quantity.scala rename to runtime/src/test/scala/coulomb/stagingquantity.scala index 0c491e40e..8fa7dcea4 100644 --- a/runtime/src/test/scala/coulomb/quantity.scala +++ b/runtime/src/test/scala/coulomb/stagingquantity.scala @@ -16,7 +16,7 @@ import coulomb.testing.CoulombSuite -class RuntimeQuantitySuite extends CoulombSuite: +class StagingRuntimeQuantitySuite extends CoulombSuite: import scala.quoted.staging import coulomb.* From 5dad792628d43647283d8bd5d0d0066430bd0914 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 11 Feb 2023 15:17:56 -0700 Subject: [PATCH 047/141] fix inverted coef ratio --- .../test/scala/coulomb/mappingquantity.scala | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 runtime/src/test/scala/coulomb/mappingquantity.scala diff --git a/runtime/src/test/scala/coulomb/mappingquantity.scala b/runtime/src/test/scala/coulomb/mappingquantity.scala new file mode 100644 index 000000000..9fad0c655 --- /dev/null +++ b/runtime/src/test/scala/coulomb/mappingquantity.scala @@ -0,0 +1,43 @@ +/* + * Copyright 2022 Erik Erlandson + * + * 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. + */ + +import coulomb.testing.CoulombSuite + +class MappingRuntimeQuantitySuite extends CoulombSuite: + import coulomb.* + import coulomb.syntax.* + import coulomb.runtime.* + + import algebra.instances.all.given + import coulomb.ops.algebra.all.{*, given} + + import coulomb.units.si.{*, given} + import coulomb.units.si.prefixes.{*, given} + import coulomb.units.us.{*, given} + + import coulomb.runtime.conversion.runtimes.mapping.MappingCoefficientRuntime + + import MappingCoefficientRuntime.{TNil, &:} + given CoefficientRuntime = MappingCoefficientRuntime.of[Kilo &: Meter &: TNil] + + test("runtimeCoefficient") { + import coulomb.policy.strict.given + val coef = runtimeCoefficient[Double]( + RuntimeUnit.of[Kilo * Meter], + RuntimeUnit.of[Meter] + ) + assert(coef.contains(1000d)) + } From f12abf63cbd5710a7c8a1f7e1d7f31c3763e279f Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 11 Feb 2023 15:19:03 -0700 Subject: [PATCH 048/141] fix inverted coef ratio --- .../scala/coulomb/runtime/conversion/runtimes/mapping.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala b/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala index 191c609aa..7c2d8cead 100644 --- a/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala +++ b/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala @@ -29,7 +29,7 @@ class MappingCoefficientRuntime( uf: RuntimeUnit, ut: RuntimeUnit ): Either[String, Rational] = - canonical(ut / uf).flatMap { qcan => + canonical(uf / ut).flatMap { qcan => val Canonical(coef, sig) = qcan if (sig.isEmpty) Right(coef) else Left(s"non-convertible units: $uf, $ut") From 20b3fe90067b3f1d9b5bdf247c3237707d909da1 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 11 Feb 2023 15:34:59 -0700 Subject: [PATCH 049/141] MappingRuntimeQuantitySuite and JS+Native builds --- build.sbt | 12 ++++++++++-- runtime/src/test/scala/coulomb/mappingquantity.scala | 3 ++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/build.sbt b/build.sbt index 5670bed77..a63188e43 100644 --- a/build.sbt +++ b/build.sbt @@ -61,7 +61,7 @@ lazy val units = crossProject(JVMPlatform, JSPlatform, NativePlatform) ) // see also: https://github.com/lampepfl/dotty/issues/7647 -lazy val runtime = crossProject(JVMPlatform /*, JSPlatform, NativePlatform */ ) +lazy val runtime = crossProject(JVMPlatform, JSPlatform, NativePlatform) .crossType(CrossType.Pure) .in(file("runtime")) .settings(name := "coulomb-runtime") @@ -69,11 +69,19 @@ lazy val runtime = crossProject(JVMPlatform /*, JSPlatform, NativePlatform */ ) core % "compile->compile;test->test", units % Test ) + .settings( + tlVersionIntroduced := Map("3" -> "0.7.4") + ) .settings(commonSettings: _*) .settings( - tlVersionIntroduced := Map("3" -> "0.7.4"), + // staging compiler is only supported on JVM + // but is also used to satisfy builds on JS and Native libraryDependencies += "org.scala-lang" %% "scala3-staging" % scalaVersion.value ) + .platformsSettings(JSPlatform, NativePlatform)( + // any unit tests using staging must be excluded from JS and Native + Test / unmanagedSources / excludeFilter := HiddenFileFilter || "*stagingquantity.scala" + ) lazy val spire = crossProject(JVMPlatform, JSPlatform, NativePlatform) .crossType(CrossType.Pure) diff --git a/runtime/src/test/scala/coulomb/mappingquantity.scala b/runtime/src/test/scala/coulomb/mappingquantity.scala index 9fad0c655..baac27fd6 100644 --- a/runtime/src/test/scala/coulomb/mappingquantity.scala +++ b/runtime/src/test/scala/coulomb/mappingquantity.scala @@ -31,7 +31,8 @@ class MappingRuntimeQuantitySuite extends CoulombSuite: import coulomb.runtime.conversion.runtimes.mapping.MappingCoefficientRuntime import MappingCoefficientRuntime.{TNil, &:} - given CoefficientRuntime = MappingCoefficientRuntime.of[Kilo &: Meter &: TNil] + given CoefficientRuntime = + MappingCoefficientRuntime.of[Kilo &: Meter &: TNil] test("runtimeCoefficient") { import coulomb.policy.strict.given From 33df4a6ac63c8e583eaabdd249ab5b2df17dcee1 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 12 Feb 2023 10:50:14 -0700 Subject: [PATCH 050/141] support module names --- .../runtime/conversion/runtimes/mapping.scala | 45 +++++++++++++++---- .../test/scala/coulomb/mappingquantity.scala | 3 +- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala b/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala index 7c2d8cead..cc748640f 100644 --- a/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala +++ b/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala @@ -111,30 +111,27 @@ object meta: def ofUTL[UTL](using Quotes, Type[UTL]): Expr[MappingCoefficientRuntime] = import quotes.reflect.* - val (bu, du) = utlClosure(TypeRepr.of[UTL]) + val (bu, du) = utlClosure(typeReprList(TypeRepr.of[UTL])) '{ new MappingCoefficientRuntime(${ Expr(bu) }, ${ Expr(du) }) } def utlClosure(using Quotes)( - utl: quotes.reflect.TypeRepr + utl: List[quotes.reflect.TypeRepr] ): (Set[RuntimeUnit.UnitType], Map[RuntimeUnit.UnitType, RuntimeUnit]) = import quotes.reflect.* utl match - case tnil if (tnil =:= TypeRepr.of[TNil]) => emptyClosure - case AppliedType(t, List(head, tail)) if (t =:= TypeRepr.of[&:]) => + case Nil => emptyClosure + case head :: tail => val (tbu, tdu) = utlClosure(tail) val (hbu, hdu) = utClosure(head) (hbu ++ tbu, hdu ++ tdu) - case _ => - report.errorAndAbort( - s"utlClosure: bad unit type list ${utl.show}" - ) - null.asInstanceOf[Nothing] def utClosure(using Quotes)( tr: quotes.reflect.TypeRepr ): (Set[RuntimeUnit.UnitType], Map[RuntimeUnit.UnitType, RuntimeUnit]) = import quotes.reflect.* tr match + case ConstantType(StringConstant(mname)) => + utlClosure(moduleUnits(mname)) case AppliedType(op, List(lu, ru)) if (op =:= TypeRepr.of[coulomb.`*`]) => val (lbu, ldu) = utClosure(lu) @@ -167,6 +164,36 @@ object meta: ) null.asInstanceOf[Nothing] + def moduleUnits(using Quotes)( + mname: String + ): List[quotes.reflect.TypeRepr] = + import quotes.reflect.* + val msym = fqFieldSymbol(mname) + if (!msym.flags.is(Flags.Module)) + report.errorAndAbort(s"$mname is not a module") + val usyms = msym.typeMembers.filter(isUnitSym) + usyms.map(_.typeRef) + + def isUnitSym(using Quotes)(sym: quotes.reflect.Symbol): Boolean = + sym.typeRef match + case baseunit() => true + case derivedunitTR(_) => true + case _ => false + + def typeReprList(using Quotes)( + tlist: quotes.reflect.TypeRepr + ): List[quotes.reflect.TypeRepr] = + import quotes.reflect.* + tlist match + case tnil if (tnil =:= TypeRepr.of[TNil]) => Nil + case AppliedType(t, List(head, tail)) if (t =:= TypeRepr.of[&:]) => + head :: typeReprList(tail) + case _ => + report.errorAndAbort( + s"utlClosure: bad type list ${tlist.show}" + ) + null.asInstanceOf[Nothing] + private val emptyMap = Map.empty[RuntimeUnit.UnitType, RuntimeUnit] diff --git a/runtime/src/test/scala/coulomb/mappingquantity.scala b/runtime/src/test/scala/coulomb/mappingquantity.scala index baac27fd6..ad7ee9443 100644 --- a/runtime/src/test/scala/coulomb/mappingquantity.scala +++ b/runtime/src/test/scala/coulomb/mappingquantity.scala @@ -32,7 +32,8 @@ class MappingRuntimeQuantitySuite extends CoulombSuite: import MappingCoefficientRuntime.{TNil, &:} given CoefficientRuntime = - MappingCoefficientRuntime.of[Kilo &: Meter &: TNil] + MappingCoefficientRuntime + .of["coulomb.units.si" &: "coulomb.units.si.prefixes" &: TNil] test("runtimeCoefficient") { import coulomb.policy.strict.given From b995750978e218d737c8dbf14e595a66b5103778 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Mon, 13 Feb 2023 17:47:50 -0700 Subject: [PATCH 051/141] either testing predicates --- .../test/scala/coulomb/testing/testing.scala | 17 +++++++++++++++ .../test/scala/coulomb/mappingquantity.scala | 21 ++++++++++++++++--- .../test/scala/coulomb/stagingquantity.scala | 2 +- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/core/src/test/scala/coulomb/testing/testing.scala b/core/src/test/scala/coulomb/testing/testing.scala index ce37bd660..7dfe7e52f 100644 --- a/core/src/test/scala/coulomb/testing/testing.scala +++ b/core/src/test/scala/coulomb/testing/testing.scala @@ -22,6 +22,11 @@ import coulomb.conversion.ValueConversion abstract class CoulombSuite extends munit.FunSuite: import coulomb.testing.types.* + extension [L, V, U](q: Either[L, Quantity[V, U]]) + inline def assertRQ[VT, UT](vt: VT): Unit = + assert(q.isRight) + q.toSeq.head.assertQ[VT, UT](vt) + extension [V, U](q: Quantity[V, U]) inline def assertQ[VT, UT](vt: VT): Unit = // checking types first @@ -58,6 +63,18 @@ abstract class CoulombSuite extends munit.FunSuite: val e = math.abs(vt) * eps.getOrElse(typeEps[V]) assertEqualsDouble(vc(q.value), vt, e) + extension [L, V](v: Either[L, V]) + inline def assertL: Unit = + assert(v.isLeft) + + inline def assertR(vt: V): Unit = + assert(v.isRight) + assertEquals(v.toSeq.head, vt) + + inline def assertRVT[VT](vt: VT): Unit = + assert(v.isRight) + v.toSeq.head.assertVT[VT](vt) + extension [V](v: V) inline def assertVT[VT](vt: VT): Unit = assertEquals(typeStr[V], typeStr[VT]) diff --git a/runtime/src/test/scala/coulomb/mappingquantity.scala b/runtime/src/test/scala/coulomb/mappingquantity.scala index ad7ee9443..88fd44d70 100644 --- a/runtime/src/test/scala/coulomb/mappingquantity.scala +++ b/runtime/src/test/scala/coulomb/mappingquantity.scala @@ -37,9 +37,24 @@ class MappingRuntimeQuantitySuite extends CoulombSuite: test("runtimeCoefficient") { import coulomb.policy.strict.given - val coef = runtimeCoefficient[Double]( + runtimeCoefficient[Double]( RuntimeUnit.of[Kilo * Meter], RuntimeUnit.of[Meter] - ) - assert(coef.contains(1000d)) + ).assertRVT[Double](1000d) + + runtimeCoefficient[Double]( + RuntimeUnit.of[Kilo * Meter], + RuntimeUnit.of[Second] + ).assertL + } + + test("toQuantity") { + import coulomb.policy.strict.given + RuntimeQuantity(1d, RuntimeUnit.of[Kilo * Meter]) + .toQuantity[Float, Meter] + .assertRQ[Float, Meter](1000f) + + RuntimeQuantity(1d, RuntimeUnit.of[Kilo * Meter]) + .toQuantity[Float, Second] + .assertL } diff --git a/runtime/src/test/scala/coulomb/stagingquantity.scala b/runtime/src/test/scala/coulomb/stagingquantity.scala index 8fa7dcea4..23bc9337f 100644 --- a/runtime/src/test/scala/coulomb/stagingquantity.scala +++ b/runtime/src/test/scala/coulomb/stagingquantity.scala @@ -43,5 +43,5 @@ class StagingRuntimeQuantitySuite extends CoulombSuite: RuntimeUnit.of[Kilo * Meter], RuntimeUnit.of[Meter] ) - assert(coef.contains(1000d)) + coef.assertRVT[Double](1000d) } From 6eac8daf0e375eb08e3c114d34e53260a56faa77 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Tue, 14 Feb 2023 16:39:22 -0700 Subject: [PATCH 052/141] factor RuntimeQuantitySuite --- .../test/scala/coulomb/mappingquantity.scala | 48 ++++------------- .../test/scala/coulomb/runtimequantity.scala | 54 +++++++++++++++++++ .../test/scala/coulomb/stagingquantity.scala | 32 +++-------- 3 files changed, 71 insertions(+), 63 deletions(-) create mode 100644 runtime/src/test/scala/coulomb/runtimequantity.scala diff --git a/runtime/src/test/scala/coulomb/mappingquantity.scala b/runtime/src/test/scala/coulomb/mappingquantity.scala index 88fd44d70..ddc9631a4 100644 --- a/runtime/src/test/scala/coulomb/mappingquantity.scala +++ b/runtime/src/test/scala/coulomb/mappingquantity.scala @@ -16,45 +16,17 @@ import coulomb.testing.CoulombSuite -class MappingRuntimeQuantitySuite extends CoulombSuite: - import coulomb.* - import coulomb.syntax.* - import coulomb.runtime.* +import coulomb.units.si.{*, given} +import coulomb.units.si.prefixes.{*, given} +import coulomb.units.us.{*, given} - import algebra.instances.all.given - import coulomb.ops.algebra.all.{*, given} +import coulomb.runtime.CoefficientRuntime +import coulomb.runtime.conversion.runtimes.mapping.MappingCoefficientRuntime - import coulomb.units.si.{*, given} - import coulomb.units.si.prefixes.{*, given} - import coulomb.units.us.{*, given} +import MappingCoefficientRuntime.{TNil, &:} - import coulomb.runtime.conversion.runtimes.mapping.MappingCoefficientRuntime +val mappingRT: CoefficientRuntime = + MappingCoefficientRuntime + .of["coulomb.units.si" &: "coulomb.units.si.prefixes" &: TNil] - import MappingCoefficientRuntime.{TNil, &:} - given CoefficientRuntime = - MappingCoefficientRuntime - .of["coulomb.units.si" &: "coulomb.units.si.prefixes" &: TNil] - - test("runtimeCoefficient") { - import coulomb.policy.strict.given - runtimeCoefficient[Double]( - RuntimeUnit.of[Kilo * Meter], - RuntimeUnit.of[Meter] - ).assertRVT[Double](1000d) - - runtimeCoefficient[Double]( - RuntimeUnit.of[Kilo * Meter], - RuntimeUnit.of[Second] - ).assertL - } - - test("toQuantity") { - import coulomb.policy.strict.given - RuntimeQuantity(1d, RuntimeUnit.of[Kilo * Meter]) - .toQuantity[Float, Meter] - .assertRQ[Float, Meter](1000f) - - RuntimeQuantity(1d, RuntimeUnit.of[Kilo * Meter]) - .toQuantity[Float, Second] - .assertL - } +class MappingRuntimeQuantitySuite extends RuntimeQuantitySuite(using mappingRT) diff --git a/runtime/src/test/scala/coulomb/runtimequantity.scala b/runtime/src/test/scala/coulomb/runtimequantity.scala new file mode 100644 index 000000000..8a3aaa745 --- /dev/null +++ b/runtime/src/test/scala/coulomb/runtimequantity.scala @@ -0,0 +1,54 @@ +/* + * Copyright 2022 Erik Erlandson + * + * 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. + */ + +import coulomb.testing.CoulombSuite +import coulomb.runtime.CoefficientRuntime + +abstract class RuntimeQuantitySuite(using CoefficientRuntime) extends CoulombSuite: + import coulomb.* + import coulomb.syntax.* + import coulomb.runtime.* + + import algebra.instances.all.given + import coulomb.ops.algebra.all.{*, given} + + import coulomb.units.si.{*, given} + import coulomb.units.si.prefixes.{*, given} + import coulomb.units.us.{*, given} + + test("runtimeCoefficient") { + import coulomb.policy.strict.given + runtimeCoefficient[Double]( + RuntimeUnit.of[Kilo * Meter], + RuntimeUnit.of[Meter] + ).assertRVT[Double](1000d) + + runtimeCoefficient[Double]( + RuntimeUnit.of[Kilo * Meter], + RuntimeUnit.of[Second] + ).assertL + } + + test("toQuantity") { + import coulomb.policy.strict.given + RuntimeQuantity(1d, RuntimeUnit.of[Kilo * Meter]) + .toQuantity[Float, Meter] + .assertRQ[Float, Meter](1000f) + + RuntimeQuantity(1d, RuntimeUnit.of[Kilo * Meter]) + .toQuantity[Float, Second] + .assertL + } diff --git a/runtime/src/test/scala/coulomb/stagingquantity.scala b/runtime/src/test/scala/coulomb/stagingquantity.scala index 23bc9337f..75b8b3aa7 100644 --- a/runtime/src/test/scala/coulomb/stagingquantity.scala +++ b/runtime/src/test/scala/coulomb/stagingquantity.scala @@ -16,32 +16,14 @@ import coulomb.testing.CoulombSuite -class StagingRuntimeQuantitySuite extends CoulombSuite: - import scala.quoted.staging +import scala.quoted.staging +import coulomb.runtime.conversion.runtimes.staging.StagingCoefficientRuntime - import coulomb.* - import coulomb.syntax.* - import coulomb.runtime.* +import coulomb.runtime.CoefficientRuntime - import algebra.instances.all.given - import coulomb.ops.algebra.all.{*, given} +val compiler: staging.Compiler = + staging.Compiler.make(classOf[staging.Compiler].getClassLoader) - import coulomb.units.si.{*, given} - import coulomb.units.si.prefixes.{*, given} - import coulomb.units.us.{*, given} +val stagingRT: CoefficientRuntime = StagingCoefficientRuntime()(using compiler) - import coulomb.runtime.conversion.runtimes.staging.StagingCoefficientRuntime - - given staging.Compiler = - staging.Compiler.make(classOf[staging.Compiler].getClassLoader) - - given CoefficientRuntime = StagingCoefficientRuntime() - - test("runtimeCoefficient") { - import coulomb.policy.strict.given - val coef = runtimeCoefficient[Double]( - RuntimeUnit.of[Kilo * Meter], - RuntimeUnit.of[Meter] - ) - coef.assertRVT[Double](1000d) - } +class StagingRuntimeQuantitySuite extends RuntimeQuantitySuite(using stagingRT) From 7fe2150e8a2629c1b75547e13cb679c5147dd4a2 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Tue, 14 Feb 2023 16:40:54 -0700 Subject: [PATCH 053/141] format --- runtime/src/test/scala/coulomb/runtimequantity.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/runtime/src/test/scala/coulomb/runtimequantity.scala b/runtime/src/test/scala/coulomb/runtimequantity.scala index 8a3aaa745..f217ab61e 100644 --- a/runtime/src/test/scala/coulomb/runtimequantity.scala +++ b/runtime/src/test/scala/coulomb/runtimequantity.scala @@ -17,7 +17,8 @@ import coulomb.testing.CoulombSuite import coulomb.runtime.CoefficientRuntime -abstract class RuntimeQuantitySuite(using CoefficientRuntime) extends CoulombSuite: +abstract class RuntimeQuantitySuite(using CoefficientRuntime) + extends CoulombSuite: import coulomb.* import coulomb.syntax.* import coulomb.runtime.* From 3122340dd8e814fc44ff82580b411cbf6dfc5eda Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 18 Feb 2023 15:21:51 -0700 Subject: [PATCH 054/141] RuntimeAdd --- .../coulomb/conversion/runtime/scala.scala | 25 ++++++++++ runtime/src/main/scala/coulomb/ops/ops.scala | 28 +++++++++++ .../main/scala/coulomb/ops/runtime/add.scala | 50 +++++++++++++++++++ .../runtime/conversion/runtimes/mapping.scala | 6 +-- .../runtime/conversion/runtimes/staging.scala | 6 +-- .../scala/coulomb/runtime/infra/meta.scala | 4 +- .../main/scala/coulomb/runtime/runtime.scala | 22 ++------ .../test/scala/coulomb/mappingquantity.scala | 4 +- .../test/scala/coulomb/runtimequantity.scala | 3 +- .../test/scala/coulomb/stagingquantity.scala | 4 +- 10 files changed, 119 insertions(+), 33 deletions(-) create mode 100644 runtime/src/main/scala/coulomb/conversion/runtime/scala.scala create mode 100644 runtime/src/main/scala/coulomb/ops/ops.scala create mode 100644 runtime/src/main/scala/coulomb/ops/runtime/add.scala diff --git a/runtime/src/main/scala/coulomb/conversion/runtime/scala.scala b/runtime/src/main/scala/coulomb/conversion/runtime/scala.scala new file mode 100644 index 000000000..ad93fec11 --- /dev/null +++ b/runtime/src/main/scala/coulomb/conversion/runtime/scala.scala @@ -0,0 +1,25 @@ +/* + * Copyright 2022 Erik Erlandson + * + * 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 coulomb.conversion.runtime + +object scala: + import _root_.scala.Conversion + import coulomb.conversion.* + import coulomb.* + import coulomb.syntax.* + + val stub = 0 diff --git a/runtime/src/main/scala/coulomb/ops/ops.scala b/runtime/src/main/scala/coulomb/ops/ops.scala new file mode 100644 index 000000000..e3f56ea7c --- /dev/null +++ b/runtime/src/main/scala/coulomb/ops/ops.scala @@ -0,0 +1,28 @@ +/* + * Copyright 2022 Erik Erlandson + * + * 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 coulomb.ops + +import scala.annotation.implicitNotFound + +import coulomb.* + +@implicitNotFound( + "Addition not defined in scope for RuntimeQuantity[${VL}] and RuntimeQuantity[${VR}]" +) +abstract class RuntimeAdd[VL, VR]: + type VO + val eval: (RuntimeQuantity[VL], RuntimeQuantity[VR]) => Either[String, RuntimeQuantity[VO]] diff --git a/runtime/src/main/scala/coulomb/ops/runtime/add.scala b/runtime/src/main/scala/coulomb/ops/runtime/add.scala new file mode 100644 index 000000000..361edc695 --- /dev/null +++ b/runtime/src/main/scala/coulomb/ops/runtime/add.scala @@ -0,0 +1,50 @@ +/* + * Copyright 2022 Erik Erlandson + * + * 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 coulomb.ops.runtime + +object add: + import scala.util.NotGiven + import scala.Conversion + + import algebra.ring.{AdditiveSemigroup, MultiplicativeSemigroup} + + import coulomb.rational.Rational + import coulomb.{RuntimeQuantity, CoefficientRuntime} + import coulomb.syntax.withRuntimeUnit + import coulomb.ops.{RuntimeAdd, ValueResolution} + import coulomb.conversion.ValueConversion + + transparent inline given ctx_runtime_add_1V[VL, VR](using + eqv: VR =:= VL, + crt: CoefficientRuntime, + vc: ValueConversion[Rational, VL], + alg: AdditiveSemigroup[VL], + mul: MultiplicativeSemigroup[VL] + ): RuntimeAdd[VL, VR] = + new infra.RTAddNC( + (ql: RuntimeQuantity[VL], qr: RuntimeQuantity[VR]) => + crt.coefficient[VL](qr.unit, ql.unit).map { coef => + alg.plus(ql.value, mul.times(coef, eqv(qr.value))).withRuntimeUnit(ql.unit) + } + ) + + object infra: + class RTAddNC[VL, VR, VOp]( + val eval: (RuntimeQuantity[VL], RuntimeQuantity[VR]) => Either[String, RuntimeQuantity[VOp]] + ) extends RuntimeAdd[VL, VR]: + type VO = VOp + diff --git a/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala b/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala index cc748640f..5a1df279a 100644 --- a/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala +++ b/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala @@ -14,11 +14,11 @@ * limitations under the License. */ -package coulomb.runtime.conversion.runtimes.mapping +package coulomb.conversion.runtimes.mapping import scala.collection.immutable.HashMap -import coulomb.runtime.* +import coulomb.* import coulomb.rational.Rational class MappingCoefficientRuntime( @@ -105,7 +105,7 @@ object meta: import scala.language.implicitConversions import coulomb.infra.meta.{*, given} - import coulomb.runtime.infra.meta.{*, given} + import coulomb.infra.runtime.meta.{*, given} import MappingCoefficientRuntime.{TNil, &:} diff --git a/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/staging.scala b/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/staging.scala index e53899d15..f1b5d1bb0 100644 --- a/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/staging.scala +++ b/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/staging.scala @@ -14,12 +14,12 @@ * limitations under the License. */ -package coulomb.runtime.conversion.runtimes.staging +package coulomb.conversion.runtimes.staging import scala.quoted.staging -import coulomb.runtime.* -import coulomb.runtime.infra.meta +import coulomb.* +import coulomb.infra.runtime.meta import coulomb.rational.Rational // a CoefficientRuntime that leverages a staging compiler to do runtime magic diff --git a/runtime/src/main/scala/coulomb/runtime/infra/meta.scala b/runtime/src/main/scala/coulomb/runtime/infra/meta.scala index 5b184dc3d..9ee790161 100644 --- a/runtime/src/main/scala/coulomb/runtime/infra/meta.scala +++ b/runtime/src/main/scala/coulomb/runtime/infra/meta.scala @@ -14,12 +14,12 @@ * limitations under the License. */ -package coulomb.runtime.infra +package coulomb.infra.runtime import scala.quoted.* import scala.util.{Try, Success, Failure} -import coulomb.runtime.* +import coulomb.* import coulomb.rational.Rational object meta: diff --git a/runtime/src/main/scala/coulomb/runtime/runtime.scala b/runtime/src/main/scala/coulomb/runtime/runtime.scala index 16182f478..0a8679ce3 100644 --- a/runtime/src/main/scala/coulomb/runtime/runtime.scala +++ b/runtime/src/main/scala/coulomb/runtime/runtime.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package coulomb.runtime +package coulomb import coulomb.{infra => _, *} import coulomb.syntax.* @@ -48,7 +48,7 @@ object RuntimeUnit: case class Mul(lhs: RuntimeUnit, rhs: RuntimeUnit) extends RuntimeUnit case class Div(num: RuntimeUnit, den: RuntimeUnit) extends RuntimeUnit case class Pow(b: RuntimeUnit, e: Rational) extends RuntimeUnit - inline def of[U]: RuntimeUnit = ${ infra.meta.unitRTU[U] } + inline def of[U]: RuntimeUnit = ${ infra.runtime.meta.unitRTU[U] } case class RuntimeQuantity[V](value: V, unit: RuntimeUnit) @@ -87,25 +87,9 @@ abstract class CoefficientRuntime: final inline def coefficientRational[UT]( uf: RuntimeUnit ): Either[String, Rational] = - infra.meta.crExpr[UT](this, uf) + infra.runtime.meta.crExpr[UT](this, uf) final def coefficient[V](uf: RuntimeUnit, ut: RuntimeUnit)(using vc: ValueConversion[Rational, V] ): Either[String, V] = this.coefficientRational(uf, ut).map(vc) - -package conversion { - abstract class RuntimeUnitConversion[V] extends (V => V) - - object RuntimeUnitConversion: - def apply[V](uf: RuntimeUnit, ut: RuntimeUnit)(using - crt: CoefficientRuntime, - vi: ValueConversion[V, Rational], - vo: ValueConversion[Rational, V] - ): Either[String, RuntimeUnitConversion[V]] = - crt.coefficientRational(uf, ut).map { coef => - new RuntimeUnitConversion[V] { - def apply(v: V): V = vo(coef * vi(v)) - } - } -} diff --git a/runtime/src/test/scala/coulomb/mappingquantity.scala b/runtime/src/test/scala/coulomb/mappingquantity.scala index ddc9631a4..9fb422fe2 100644 --- a/runtime/src/test/scala/coulomb/mappingquantity.scala +++ b/runtime/src/test/scala/coulomb/mappingquantity.scala @@ -20,8 +20,8 @@ import coulomb.units.si.{*, given} import coulomb.units.si.prefixes.{*, given} import coulomb.units.us.{*, given} -import coulomb.runtime.CoefficientRuntime -import coulomb.runtime.conversion.runtimes.mapping.MappingCoefficientRuntime +import coulomb.CoefficientRuntime +import coulomb.conversion.runtimes.mapping.MappingCoefficientRuntime import MappingCoefficientRuntime.{TNil, &:} diff --git a/runtime/src/test/scala/coulomb/runtimequantity.scala b/runtime/src/test/scala/coulomb/runtimequantity.scala index f217ab61e..d1b6dadd1 100644 --- a/runtime/src/test/scala/coulomb/runtimequantity.scala +++ b/runtime/src/test/scala/coulomb/runtimequantity.scala @@ -15,13 +15,12 @@ */ import coulomb.testing.CoulombSuite -import coulomb.runtime.CoefficientRuntime +import coulomb.CoefficientRuntime abstract class RuntimeQuantitySuite(using CoefficientRuntime) extends CoulombSuite: import coulomb.* import coulomb.syntax.* - import coulomb.runtime.* import algebra.instances.all.given import coulomb.ops.algebra.all.{*, given} diff --git a/runtime/src/test/scala/coulomb/stagingquantity.scala b/runtime/src/test/scala/coulomb/stagingquantity.scala index 75b8b3aa7..2ce5d9413 100644 --- a/runtime/src/test/scala/coulomb/stagingquantity.scala +++ b/runtime/src/test/scala/coulomb/stagingquantity.scala @@ -17,9 +17,9 @@ import coulomb.testing.CoulombSuite import scala.quoted.staging -import coulomb.runtime.conversion.runtimes.staging.StagingCoefficientRuntime +import coulomb.conversion.runtimes.staging.StagingCoefficientRuntime -import coulomb.runtime.CoefficientRuntime +import coulomb.CoefficientRuntime val compiler: staging.Compiler = staging.Compiler.make(classOf[staging.Compiler].getClassLoader) From 0f9c7036b8bf26a11f1a6e2e782a3c9291b27a96 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 18 Feb 2023 15:22:43 -0700 Subject: [PATCH 055/141] format --- .../coulomb/conversion/runtime/scala.scala | 2 +- runtime/src/main/scala/coulomb/ops/ops.scala | 5 ++++- .../main/scala/coulomb/ops/runtime/add.scala | 18 ++++++++++-------- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/runtime/src/main/scala/coulomb/conversion/runtime/scala.scala b/runtime/src/main/scala/coulomb/conversion/runtime/scala.scala index ad93fec11..5b237edf3 100644 --- a/runtime/src/main/scala/coulomb/conversion/runtime/scala.scala +++ b/runtime/src/main/scala/coulomb/conversion/runtime/scala.scala @@ -22,4 +22,4 @@ object scala: import coulomb.* import coulomb.syntax.* - val stub = 0 + val stub = 0 diff --git a/runtime/src/main/scala/coulomb/ops/ops.scala b/runtime/src/main/scala/coulomb/ops/ops.scala index e3f56ea7c..c8f44ab33 100644 --- a/runtime/src/main/scala/coulomb/ops/ops.scala +++ b/runtime/src/main/scala/coulomb/ops/ops.scala @@ -25,4 +25,7 @@ import coulomb.* ) abstract class RuntimeAdd[VL, VR]: type VO - val eval: (RuntimeQuantity[VL], RuntimeQuantity[VR]) => Either[String, RuntimeQuantity[VO]] + val eval: ( + RuntimeQuantity[VL], + RuntimeQuantity[VR] + ) => Either[String, RuntimeQuantity[VO]] diff --git a/runtime/src/main/scala/coulomb/ops/runtime/add.scala b/runtime/src/main/scala/coulomb/ops/runtime/add.scala index 361edc695..d3ee28670 100644 --- a/runtime/src/main/scala/coulomb/ops/runtime/add.scala +++ b/runtime/src/main/scala/coulomb/ops/runtime/add.scala @@ -27,7 +27,7 @@ object add: import coulomb.syntax.withRuntimeUnit import coulomb.ops.{RuntimeAdd, ValueResolution} import coulomb.conversion.ValueConversion - + transparent inline given ctx_runtime_add_1V[VL, VR](using eqv: VR =:= VL, crt: CoefficientRuntime, @@ -35,16 +35,18 @@ object add: alg: AdditiveSemigroup[VL], mul: MultiplicativeSemigroup[VL] ): RuntimeAdd[VL, VR] = - new infra.RTAddNC( - (ql: RuntimeQuantity[VL], qr: RuntimeQuantity[VR]) => - crt.coefficient[VL](qr.unit, ql.unit).map { coef => - alg.plus(ql.value, mul.times(coef, eqv(qr.value))).withRuntimeUnit(ql.unit) - } + new infra.RTAddNC((ql: RuntimeQuantity[VL], qr: RuntimeQuantity[VR]) => + crt.coefficient[VL](qr.unit, ql.unit).map { coef => + alg.plus(ql.value, mul.times(coef, eqv(qr.value))) + .withRuntimeUnit(ql.unit) + } ) object infra: class RTAddNC[VL, VR, VOp]( - val eval: (RuntimeQuantity[VL], RuntimeQuantity[VR]) => Either[String, RuntimeQuantity[VOp]] + val eval: ( + RuntimeQuantity[VL], + RuntimeQuantity[VR] + ) => Either[String, RuntimeQuantity[VOp]] ) extends RuntimeAdd[VL, VR]: type VO = VOp - From 63130015f522b287dba71541ba3efc398a4efac4 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 19 Feb 2023 08:11:29 -0700 Subject: [PATCH 056/141] addition --- .../coulomb/conversion/runtime/scala.scala | 9 ++++++- .../main/scala/coulomb/ops/runtime/add.scala | 21 +++++++++++++++- .../main/scala/coulomb/runtime/runtime.scala | 24 ++++++++++++------- .../test/scala/coulomb/runtimequantity.scala | 9 +++++++ 4 files changed, 52 insertions(+), 11 deletions(-) diff --git a/runtime/src/main/scala/coulomb/conversion/runtime/scala.scala b/runtime/src/main/scala/coulomb/conversion/runtime/scala.scala index 5b237edf3..1c1646b65 100644 --- a/runtime/src/main/scala/coulomb/conversion/runtime/scala.scala +++ b/runtime/src/main/scala/coulomb/conversion/runtime/scala.scala @@ -22,4 +22,11 @@ object scala: import coulomb.* import coulomb.syntax.* - val stub = 0 + given ctx_RuntimeQuantity_Conversion_1V[V] + : Conversion[RuntimeQuantity[V], RuntimeQuantity[V]] = + (q: RuntimeQuantity[V]) => q + + given ctx_RuntimeQuantity_Conversion_2V[VF, VT](using + vc: ValueConversion[VF, VT] + ): Conversion[RuntimeQuantity[VF], RuntimeQuantity[VT]] = + (q: RuntimeQuantity[VF]) => RuntimeQuantity(vc(q.value), q.unit) diff --git a/runtime/src/main/scala/coulomb/ops/runtime/add.scala b/runtime/src/main/scala/coulomb/ops/runtime/add.scala index d3ee28670..da4838b56 100644 --- a/runtime/src/main/scala/coulomb/ops/runtime/add.scala +++ b/runtime/src/main/scala/coulomb/ops/runtime/add.scala @@ -31,7 +31,7 @@ object add: transparent inline given ctx_runtime_add_1V[VL, VR](using eqv: VR =:= VL, crt: CoefficientRuntime, - vc: ValueConversion[Rational, VL], + vcr: ValueConversion[Rational, VL], alg: AdditiveSemigroup[VL], mul: MultiplicativeSemigroup[VL] ): RuntimeAdd[VL, VR] = @@ -42,6 +42,25 @@ object add: } ) + transparent inline given ctx_runtime_add_2V[VL, VR](using + nev: NotGiven[VR =:= VL], + crt: CoefficientRuntime, + vres: ValueResolution[VL, VR], + icl: Conversion[RuntimeQuantity[VL], RuntimeQuantity[vres.VO]], + icr: Conversion[RuntimeQuantity[VR], RuntimeQuantity[vres.VO]], + vcr: ValueConversion[Rational, vres.VO], + alg: AdditiveSemigroup[vres.VO], + mul: MultiplicativeSemigroup[vres.VO] + ): RuntimeAdd[VL, VR] = + new infra.RTAddNC((ql: RuntimeQuantity[VL], qr: RuntimeQuantity[VR]) => + val qlo = icl(ql) + val qro = icr(qr) + crt.coefficient[vres.VO](qro.unit, qlo.unit).map { coef => + alg.plus(qlo.value, mul.times(coef, qro.value)) + .withRuntimeUnit(qlo.unit) + } + ) + object infra: class RTAddNC[VL, VR, VOp]( val eval: ( diff --git a/runtime/src/main/scala/coulomb/runtime/runtime.scala b/runtime/src/main/scala/coulomb/runtime/runtime.scala index 0a8679ce3..df50aba58 100644 --- a/runtime/src/main/scala/coulomb/runtime/runtime.scala +++ b/runtime/src/main/scala/coulomb/runtime/runtime.scala @@ -53,18 +53,24 @@ object RuntimeUnit: case class RuntimeQuantity[V](value: V, unit: RuntimeUnit) object RuntimeQuantity: + import coulomb.ops.* + inline def apply[V, U](q: Quantity[V, U]): RuntimeQuantity[V] = RuntimeQuantity(q.value, RuntimeUnit.of[U]) -extension [VL](ql: RuntimeQuantity[VL]) - inline def toQuantity[VR, UR](using - crt: CoefficientRuntime, - vi: ValueConversion[VL, Rational], - vo: ValueConversion[Rational, VR] - ): Either[String, Quantity[VR, UR]] = - crt.coefficientRational[UR](ql.unit).map { coef => - vo(coef * vi(ql.value)).withUnit[UR] - } + extension [VL](ql: RuntimeQuantity[VL]) + inline def toQuantity[VR, UR](using + crt: CoefficientRuntime, + vi: ValueConversion[VL, Rational], + vo: ValueConversion[Rational, VR] + ): Either[String, Quantity[VR, UR]] = + crt.coefficientRational[UR](ql.unit).map { coef => + vo(coef * vi(ql.value)).withUnit[UR] + } + + transparent inline def +[VR](qr: RuntimeQuantity[VR])(using + add: RuntimeAdd[VL, VR] + ): Either[String, RuntimeQuantity[add.VO]] = add.eval(ql, qr) package syntax { extension [V](v: V) diff --git a/runtime/src/test/scala/coulomb/runtimequantity.scala b/runtime/src/test/scala/coulomb/runtimequantity.scala index d1b6dadd1..862301991 100644 --- a/runtime/src/test/scala/coulomb/runtimequantity.scala +++ b/runtime/src/test/scala/coulomb/runtimequantity.scala @@ -52,3 +52,12 @@ abstract class RuntimeQuantitySuite(using CoefficientRuntime) .toQuantity[Float, Second] .assertL } + + test("addition") { + import coulomb.policy.strict.given + import coulomb.ops.runtime.add.given + + (RuntimeQuantity(1d, RuntimeUnit.of[Meter]) + + RuntimeQuantity(1d, RuntimeUnit.of[Kilo * Meter])) + .assertR(RuntimeQuantity(1001d, RuntimeUnit.of[Meter])) + } From 6c1affcaa2b542ff49fb4b669a2e9bc9fa1840be Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 19 Feb 2023 12:51:27 -0700 Subject: [PATCH 057/141] runtime policies --- .../main/scala/coulomb/ops/runtime/all.scala | 20 ++++++++++++++++ .../policy/overlay/runtime/policy.scala | 24 +++++++++++++++++++ .../test/scala/coulomb/runtimequantity.scala | 4 ++-- 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 runtime/src/main/scala/coulomb/ops/runtime/all.scala create mode 100644 runtime/src/main/scala/coulomb/policy/overlay/runtime/policy.scala diff --git a/runtime/src/main/scala/coulomb/ops/runtime/all.scala b/runtime/src/main/scala/coulomb/ops/runtime/all.scala new file mode 100644 index 000000000..4cc486f60 --- /dev/null +++ b/runtime/src/main/scala/coulomb/ops/runtime/all.scala @@ -0,0 +1,20 @@ +/* + * Copyright 2022 Erik Erlandson + * + * 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 coulomb.ops.runtime + +object all: + export coulomb.ops.runtime.add.given diff --git a/runtime/src/main/scala/coulomb/policy/overlay/runtime/policy.scala b/runtime/src/main/scala/coulomb/policy/overlay/runtime/policy.scala new file mode 100644 index 000000000..3f93bf7fa --- /dev/null +++ b/runtime/src/main/scala/coulomb/policy/overlay/runtime/policy.scala @@ -0,0 +1,24 @@ +/* + * Copyright 2022 Erik Erlandson + * + * 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 coulomb.policy.overlay.runtime + +object strict: + export coulomb.ops.runtime.all.given + +object standard: + export coulomb.ops.runtime.all.given + export coulomb.conversion.runtime.scala.given diff --git a/runtime/src/test/scala/coulomb/runtimequantity.scala b/runtime/src/test/scala/coulomb/runtimequantity.scala index 862301991..058878a71 100644 --- a/runtime/src/test/scala/coulomb/runtimequantity.scala +++ b/runtime/src/test/scala/coulomb/runtimequantity.scala @@ -53,9 +53,9 @@ abstract class RuntimeQuantitySuite(using CoefficientRuntime) .assertL } - test("addition") { + test("addition strict") { import coulomb.policy.strict.given - import coulomb.ops.runtime.add.given + import coulomb.policy.overlay.runtime.strict.given (RuntimeQuantity(1d, RuntimeUnit.of[Meter]) + RuntimeQuantity(1d, RuntimeUnit.of[Kilo * Meter])) From adca749790f55c0140df57d12bc765b864486682 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 19 Feb 2023 13:15:28 -0700 Subject: [PATCH 058/141] standard addition --- .../test/scala/coulomb/runtimequantity.scala | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/runtime/src/test/scala/coulomb/runtimequantity.scala b/runtime/src/test/scala/coulomb/runtimequantity.scala index 058878a71..1847b5b28 100644 --- a/runtime/src/test/scala/coulomb/runtimequantity.scala +++ b/runtime/src/test/scala/coulomb/runtimequantity.scala @@ -60,4 +60,24 @@ abstract class RuntimeQuantitySuite(using CoefficientRuntime) (RuntimeQuantity(1d, RuntimeUnit.of[Meter]) + RuntimeQuantity(1d, RuntimeUnit.of[Kilo * Meter])) .assertR(RuntimeQuantity(1001d, RuntimeUnit.of[Meter])) + + (RuntimeQuantity(1d, RuntimeUnit.of[Second]) + + RuntimeQuantity(1d, RuntimeUnit.of[Kilo * Meter])).assertL + + assertCE(""" + (RuntimeQuantity(1, RuntimeUnit.of[Meter]) + + RuntimeQuantity(1d, RuntimeUnit.of[Kilo * Meter])) + """) + } + + test("addition standard") { + import coulomb.policy.standard.given + import coulomb.policy.overlay.runtime.standard.given + + (RuntimeQuantity(1, RuntimeUnit.of[Meter]) + + RuntimeQuantity(1d, RuntimeUnit.of[Kilo * Meter])) + .assertR(RuntimeQuantity(1001d, RuntimeUnit.of[Meter])) + + (RuntimeQuantity(1, RuntimeUnit.of[Second]) + + RuntimeQuantity(1d, RuntimeUnit.of[Kilo * Meter])).assertL } From ad8d7ef1b8effba89c16aeea6cc2886334d2b38a Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Wed, 22 Feb 2023 17:01:07 -0700 Subject: [PATCH 059/141] add some syntax options --- .../main/scala/coulomb/runtime/runtime.scala | 35 ++++++++++++------- .../test/scala/coulomb/runtimequantity.scala | 19 ++++------ 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/runtime/src/main/scala/coulomb/runtime/runtime.scala b/runtime/src/main/scala/coulomb/runtime/runtime.scala index df50aba58..84bcf6043 100644 --- a/runtime/src/main/scala/coulomb/runtime/runtime.scala +++ b/runtime/src/main/scala/coulomb/runtime/runtime.scala @@ -50,6 +50,21 @@ object RuntimeUnit: case class Pow(b: RuntimeUnit, e: Rational) extends RuntimeUnit inline def of[U]: RuntimeUnit = ${ infra.runtime.meta.unitRTU[U] } +def runtimeCoefficient[V](uf: RuntimeUnit, ut: RuntimeUnit)(using + crt: CoefficientRuntime, + vc: ValueConversion[Rational, V] +): Either[String, V] = + crt.coefficient[V](uf, ut) + +package syntax { + extension [V](v: V) + inline def withRuntimeUnit(u: RuntimeUnit): RuntimeQuantity[V] = + RuntimeQuantity(v, u) + + inline def withRuntimeUnit[U]: RuntimeQuantity[V] = + RuntimeQuantity(v, RuntimeUnit.of[U]) +} + case class RuntimeQuantity[V](value: V, unit: RuntimeUnit) object RuntimeQuantity: @@ -58,6 +73,14 @@ object RuntimeQuantity: inline def apply[V, U](q: Quantity[V, U]): RuntimeQuantity[V] = RuntimeQuantity(q.value, RuntimeUnit.of[U]) + inline def apply[U](using a: Applier[U]) = a + + class Applier[U]: + inline def apply[V](v: V): RuntimeQuantity[V] = + RuntimeQuantity(v, RuntimeUnit.of[U]) + object Applier: + given ctx_Applier[U]: Applier[U] = new Applier[U] + extension [VL](ql: RuntimeQuantity[VL]) inline def toQuantity[VR, UR](using crt: CoefficientRuntime, @@ -72,18 +95,6 @@ object RuntimeQuantity: add: RuntimeAdd[VL, VR] ): Either[String, RuntimeQuantity[add.VO]] = add.eval(ql, qr) -package syntax { - extension [V](v: V) - inline def withRuntimeUnit(u: RuntimeUnit): RuntimeQuantity[V] = - RuntimeQuantity(v, u) -} - -def runtimeCoefficient[V](uf: RuntimeUnit, ut: RuntimeUnit)(using - crt: CoefficientRuntime, - vc: ValueConversion[Rational, V] -): Either[String, V] = - crt.coefficient[V](uf, ut) - abstract class CoefficientRuntime: def coefficientRational( uf: RuntimeUnit, diff --git a/runtime/src/test/scala/coulomb/runtimequantity.scala b/runtime/src/test/scala/coulomb/runtimequantity.scala index 1847b5b28..6f1950cc1 100644 --- a/runtime/src/test/scala/coulomb/runtimequantity.scala +++ b/runtime/src/test/scala/coulomb/runtimequantity.scala @@ -57,16 +57,13 @@ abstract class RuntimeQuantitySuite(using CoefficientRuntime) import coulomb.policy.strict.given import coulomb.policy.overlay.runtime.strict.given - (RuntimeQuantity(1d, RuntimeUnit.of[Meter]) - + RuntimeQuantity(1d, RuntimeUnit.of[Kilo * Meter])) - .assertR(RuntimeQuantity(1001d, RuntimeUnit.of[Meter])) + (1d.withRuntimeUnit[Meter] + 1d.withRuntimeUnit[Kilo * Meter]) + .assertR(RuntimeQuantity[Meter](1001d)) - (RuntimeQuantity(1d, RuntimeUnit.of[Second]) - + RuntimeQuantity(1d, RuntimeUnit.of[Kilo * Meter])).assertL + (1d.withRuntimeUnit[Second] + 1d.withRuntimeUnit[Kilo * Meter]).assertL assertCE(""" - (RuntimeQuantity(1, RuntimeUnit.of[Meter]) - + RuntimeQuantity(1d, RuntimeUnit.of[Kilo * Meter])) + (1.withRuntimeUnit[Meter] + 1d.withRuntimeUnit[Kilo * Meter]) """) } @@ -74,10 +71,8 @@ abstract class RuntimeQuantitySuite(using CoefficientRuntime) import coulomb.policy.standard.given import coulomb.policy.overlay.runtime.standard.given - (RuntimeQuantity(1, RuntimeUnit.of[Meter]) - + RuntimeQuantity(1d, RuntimeUnit.of[Kilo * Meter])) - .assertR(RuntimeQuantity(1001d, RuntimeUnit.of[Meter])) + (1.withRuntimeUnit[Meter] + 1d.withRuntimeUnit[Kilo * Meter]) + .assertR(RuntimeQuantity[Meter](1001d)) - (RuntimeQuantity(1, RuntimeUnit.of[Second]) - + RuntimeQuantity(1d, RuntimeUnit.of[Kilo * Meter])).assertL + (1.withRuntimeUnit[Second] + 1d.withRuntimeUnit[Kilo * Meter]).assertL } From 5e9658d08f4c234fabe14a2e33c59a3fae9866e9 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 25 Feb 2023 13:23:22 -0700 Subject: [PATCH 060/141] stub coulomb-pureconfig --- .github/workflows/ci.yml | 4 ++-- build.sbt | 18 +++++++++++++++++ .../src/main/scala/coulomb/pureconfig.scala | 20 +++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 pureconfig/src/main/scala/coulomb/pureconfig.scala diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5ff1206f0..e3242574d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,11 +80,11 @@ jobs: - name: Make target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/scala3') - run: mkdir -p spire/.js/target spire/.jvm/target testkit/.native/target units/.jvm/target testkit/.js/target parser/.jvm/target unidocs/target parser/.js/target core/.native/target spire/.native/target parser/.native/target core/.js/target units/.native/target core/.jvm/target refined/.native/target refined/.js/target refined/.jvm/target units/.js/target testkit/.jvm/target project/target + run: mkdir -p runtime/.js/target spire/.js/target spire/.jvm/target benchmarks/target testkit/.native/target target pureconfig/.native/target all/target units/.jvm/target runtime/.native/target testkit/.js/target unidocs/target .js/target core/.native/target site/target pureconfig/.jvm/target spire/.native/target core/.js/target units/.native/target runtime/.jvm/target core/.jvm/target refined/.native/target .jvm/target .native/target refined/.js/target refined/.jvm/target units/.js/target testkit/.jvm/target pureconfig/.js/target project/target - name: Compress target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/scala3') - run: tar cf targets.tar spire/.js/target spire/.jvm/target testkit/.native/target units/.jvm/target testkit/.js/target parser/.jvm/target unidocs/target parser/.js/target core/.native/target spire/.native/target parser/.native/target core/.js/target units/.native/target core/.jvm/target refined/.native/target refined/.js/target refined/.jvm/target units/.js/target testkit/.jvm/target project/target + run: tar cf targets.tar runtime/.js/target spire/.js/target spire/.jvm/target benchmarks/target testkit/.native/target target pureconfig/.native/target all/target units/.jvm/target runtime/.native/target testkit/.js/target unidocs/target .js/target core/.native/target site/target pureconfig/.jvm/target spire/.native/target core/.js/target units/.native/target runtime/.jvm/target core/.jvm/target refined/.native/target .jvm/target .native/target refined/.js/target refined/.jvm/target units/.js/target testkit/.jvm/target pureconfig/.js/target project/target - name: Upload target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/scala3') diff --git a/build.sbt b/build.sbt index a63188e43..1f05dfa97 100644 --- a/build.sbt +++ b/build.sbt @@ -83,6 +83,23 @@ lazy val runtime = crossProject(JVMPlatform, JSPlatform, NativePlatform) Test / unmanagedSources / excludeFilter := HiddenFileFilter || "*stagingquantity.scala" ) +lazy val pureconfig = crossProject(JVMPlatform, JSPlatform, NativePlatform) + .crossType(CrossType.Pure) + .in(file("pureconfig")) + .settings(name := "coulomb-pureconfig") + .dependsOn( + core % "compile->compile;test->test", + runtime, + units % Test + ) + .settings( + tlVersionIntroduced := Map("3" -> "0.7.4") + ) + .settings(commonSettings: _*) + .settings( + libraryDependencies += "com.github.pureconfig" %% "pureconfig-core" % "0.17.2" + ) + lazy val spire = crossProject(JVMPlatform, JSPlatform, NativePlatform) .crossType(CrossType.Pure) .in(file("spire")) @@ -126,6 +143,7 @@ lazy val all = project core.jvm, units.jvm, runtime.jvm, + pureconfig.jvm, spire.jvm, refined.jvm ) // scala repl only needs JVMPlatform subproj builds diff --git a/pureconfig/src/main/scala/coulomb/pureconfig.scala b/pureconfig/src/main/scala/coulomb/pureconfig.scala new file mode 100644 index 000000000..6c9640f47 --- /dev/null +++ b/pureconfig/src/main/scala/coulomb/pureconfig.scala @@ -0,0 +1,20 @@ +/* + * Copyright 2022 Erik Erlandson + * + * 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 coulomb + +object pureconfig: + val stub = 0 From b601c59d9298dec9ea158a0ca6251774509ef903 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 25 Feb 2023 16:12:10 -0700 Subject: [PATCH 061/141] ConfigReader[RuntimeUnit] --- .../src/main/scala/coulomb/pureconfig.scala | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/pureconfig/src/main/scala/coulomb/pureconfig.scala b/pureconfig/src/main/scala/coulomb/pureconfig.scala index 6c9640f47..80df2b5f2 100644 --- a/pureconfig/src/main/scala/coulomb/pureconfig.scala +++ b/pureconfig/src/main/scala/coulomb/pureconfig.scala @@ -16,5 +16,45 @@ package coulomb +import _root_.pureconfig.* + +import coulomb.{infra => _, *} +import coulomb.rational.Rational + object pureconfig: - val stub = 0 + // it would be nice to handle polymorphic inputs + // https://github.com/pureconfig/pureconfig/issues/1472 + given rationalReader: ConfigReader[Rational] = + ConfigReader.forProduct2("n", "d") { (n: BigInt, d: BigInt) => + Rational(n, d) + } + + given runtimeUnitReader: ConfigReader[RuntimeUnit] = + ConfigReader.fromCursor { cur => + for { + objcur <- cur.asObjectCursor + typecur <- objcur.atKey("type") + typestr <- typecur.asString + u <- infra.readByType(typestr, objcur) + } yield u + } + + object infra: + import _root_.pureconfig.error.CannotConvert + + def readByType(typ: String, cur: ConfigObjectCursor): ConfigReader.Result[RuntimeUnit] = + typ match + case "const" => unitConstReader.from(cur) + case "type" => unitTypeReader.from(cur) + case "mul" => mulReader.from(cur) + case "div" => divReader.from(cur) + case "pow" => powReader.from(cur) + case t => + cur.failed(CannotConvert(cur.objValue.toString, "RuntimeUnit", s"unknown type $t")) + + val unitConstReader = ConfigReader.forProduct1("value")(RuntimeUnit.UnitConst(_)) + val unitTypeReader = ConfigReader.forProduct1("path")(RuntimeUnit.UnitType(_)) + val mulReader = ConfigReader.forProduct2("lhs", "rhs")(RuntimeUnit.Mul(_, _)) + val divReader = ConfigReader.forProduct2("num", "den")(RuntimeUnit.Div(_, _)) + val powReader = ConfigReader.forProduct2("b", "e")(RuntimeUnit.Pow(_, _)) + From 8dbafc67b874167987d5b8bf9371782d7c92fb6a Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 26 Feb 2023 11:41:43 -0700 Subject: [PATCH 062/141] ConfigReader[RuntimeUnit] and ConfigWriter[RuntimeUnit] --- .../src/main/scala/coulomb/pureconfig.scala | 47 +++++++++++++++---- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/pureconfig/src/main/scala/coulomb/pureconfig.scala b/pureconfig/src/main/scala/coulomb/pureconfig.scala index 80df2b5f2..f27db1509 100644 --- a/pureconfig/src/main/scala/coulomb/pureconfig.scala +++ b/pureconfig/src/main/scala/coulomb/pureconfig.scala @@ -29,32 +29,63 @@ object pureconfig: Rational(n, d) } + given rationalWriter: ConfigWriter[Rational] = + ConfigWriter.forProduct2("n", "d") { (r: Rational) => + (r.n, r.d) + } + given runtimeUnitReader: ConfigReader[RuntimeUnit] = - ConfigReader.fromCursor { cur => + ConfigReader.fromCursor { xcur => for { - objcur <- cur.asObjectCursor - typecur <- objcur.atKey("type") + cur <- xcur.asObjectCursor + typecur <- cur.atKey("type") typestr <- typecur.asString - u <- infra.readByType(typestr, objcur) + u <- infra.readByType(typestr, cur) } yield u } + given runtimeUnitWriter: ConfigWriter[RuntimeUnit] = + ConfigWriter.fromFunction { (x: RuntimeUnit) => + x match + case u: RuntimeUnit.UnitConst => infra.constWriter.to(u) + case u: RuntimeUnit.UnitType => infra.typeWriter.to(u) + case u: RuntimeUnit.Mul => infra.mulWriter.to(u) + case u: RuntimeUnit.Div => infra.divWriter.to(u) + case u: RuntimeUnit.Pow => infra.powWriter.to(u) + } + object infra: import _root_.pureconfig.error.CannotConvert def readByType(typ: String, cur: ConfigObjectCursor): ConfigReader.Result[RuntimeUnit] = typ match - case "const" => unitConstReader.from(cur) - case "type" => unitTypeReader.from(cur) + case "const" => constReader.from(cur) + case "type" => typeReader.from(cur) case "mul" => mulReader.from(cur) case "div" => divReader.from(cur) case "pow" => powReader.from(cur) case t => cur.failed(CannotConvert(cur.objValue.toString, "RuntimeUnit", s"unknown type $t")) - val unitConstReader = ConfigReader.forProduct1("value")(RuntimeUnit.UnitConst(_)) - val unitTypeReader = ConfigReader.forProduct1("path")(RuntimeUnit.UnitType(_)) + val constReader = ConfigReader.forProduct1("value")(RuntimeUnit.UnitConst(_)) + val typeReader = ConfigReader.forProduct1("path")(RuntimeUnit.UnitType(_)) val mulReader = ConfigReader.forProduct2("lhs", "rhs")(RuntimeUnit.Mul(_, _)) val divReader = ConfigReader.forProduct2("num", "den")(RuntimeUnit.Div(_, _)) val powReader = ConfigReader.forProduct2("b", "e")(RuntimeUnit.Pow(_, _)) + val constWriter = ConfigWriter.forProduct2("type", "value") { + (u: RuntimeUnit.UnitConst) => ("const", u.value) + } + val typeWriter = ConfigWriter.forProduct2("type", "path") { + (u: RuntimeUnit.UnitType) => ("type", u.path) + } + val mulWriter = ConfigWriter.forProduct3("type", "lhs", "rhs") { + (u: RuntimeUnit.Mul) => ("mul", u.lhs, u.rhs) + } + val divWriter = ConfigWriter.forProduct3("type", "num", "den") { + (u: RuntimeUnit.Div) => ("div", u.num, u.den) + } + val powWriter = ConfigWriter.forProduct3("type", "b", "e") { + (u: RuntimeUnit.Pow) => ("pow", u.b, u.e) + } + From 10aaa112fab8a03978c6628bfde577369c18f202 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Wed, 1 Mar 2023 15:39:05 -0700 Subject: [PATCH 063/141] stub coulomb-parser --- .github/workflows/ci.yml | 4 +-- build.sbt | 29 ++++++++++++++++++- parser/src/main/scala/coulomb/parser.scala | 20 +++++++++++++ .../src/main/scala/coulomb/pureconfig.scala | 3 ++ 4 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 parser/src/main/scala/coulomb/parser.scala diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e3242574d..78511f1d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,11 +80,11 @@ jobs: - name: Make target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/scala3') - run: mkdir -p runtime/.js/target spire/.js/target spire/.jvm/target benchmarks/target testkit/.native/target target pureconfig/.native/target all/target units/.jvm/target runtime/.native/target testkit/.js/target unidocs/target .js/target core/.native/target site/target pureconfig/.jvm/target spire/.native/target core/.js/target units/.native/target runtime/.jvm/target core/.jvm/target refined/.native/target .jvm/target .native/target refined/.js/target refined/.jvm/target units/.js/target testkit/.jvm/target pureconfig/.js/target project/target + run: mkdir -p runtime/.js/target spire/.js/target spire/.jvm/target benchmarks/target testkit/.native/target target pureconfig/.native/target all/target units/.jvm/target runtime/.native/target testkit/.js/target parser/.jvm/target unidocs/target .js/target parser/.js/target core/.native/target site/target pureconfig/.jvm/target spire/.native/target parser/.native/target core/.js/target units/.native/target runtime/.jvm/target core/.jvm/target refined/.native/target .jvm/target .native/target refined/.js/target refined/.jvm/target units/.js/target testkit/.jvm/target pureconfig/.js/target project/target - name: Compress target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/scala3') - run: tar cf targets.tar runtime/.js/target spire/.js/target spire/.jvm/target benchmarks/target testkit/.native/target target pureconfig/.native/target all/target units/.jvm/target runtime/.native/target testkit/.js/target unidocs/target .js/target core/.native/target site/target pureconfig/.jvm/target spire/.native/target core/.js/target units/.native/target runtime/.jvm/target core/.jvm/target refined/.native/target .jvm/target .native/target refined/.js/target refined/.jvm/target units/.js/target testkit/.jvm/target pureconfig/.js/target project/target + run: tar cf targets.tar runtime/.js/target spire/.js/target spire/.jvm/target benchmarks/target testkit/.native/target target pureconfig/.native/target all/target units/.jvm/target runtime/.native/target testkit/.js/target parser/.jvm/target unidocs/target .js/target parser/.js/target core/.native/target site/target pureconfig/.jvm/target spire/.native/target parser/.native/target core/.js/target units/.native/target runtime/.jvm/target core/.jvm/target refined/.native/target .jvm/target .native/target refined/.js/target refined/.jvm/target units/.js/target testkit/.jvm/target pureconfig/.js/target project/target - name: Upload target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/scala3') diff --git a/build.sbt b/build.sbt index 1f05dfa97..da5a19f3a 100644 --- a/build.sbt +++ b/build.sbt @@ -83,6 +83,23 @@ lazy val runtime = crossProject(JVMPlatform, JSPlatform, NativePlatform) Test / unmanagedSources / excludeFilter := HiddenFileFilter || "*stagingquantity.scala" ) +lazy val parser = crossProject(JVMPlatform, JSPlatform, NativePlatform) + .crossType(CrossType.Pure) + .in(file("parser")) + .settings(name := "coulomb-parser") + .dependsOn( + core % "compile->compile;test->test", + runtime, + units % Test + ) + .settings( + tlVersionIntroduced := Map("3" -> "0.7.4") + ) + .settings(commonSettings: _*) + .settings( + libraryDependencies += "org.typelevel" %% "cats-parse" % "0.3.7" + ) + lazy val pureconfig = crossProject(JVMPlatform, JSPlatform, NativePlatform) .crossType(CrossType.Pure) .in(file("pureconfig")) @@ -90,6 +107,7 @@ lazy val pureconfig = crossProject(JVMPlatform, JSPlatform, NativePlatform) .dependsOn( core % "compile->compile;test->test", runtime, + parser, units % Test ) .settings( @@ -143,6 +161,7 @@ lazy val all = project core.jvm, units.jvm, runtime.jvm, + parser.jvm, pureconfig.jvm, spire.jvm, refined.jvm @@ -166,7 +185,15 @@ lazy val unidocs = project // http://localhost:4242 lazy val docs = project .in(file("site")) - .dependsOn(core.jvm, units.jvm, spire.jvm, refined.jvm) + .dependsOn( + core.jvm, + units.jvm, + runtime.jvm, + parser.jvm, + pureconfig.jvm, + spire.jvm, + refined.jvm + ) .enablePlugins(TypelevelSitePlugin) .settings( // turn off the new -W warnings in mdoc scala compilations diff --git a/parser/src/main/scala/coulomb/parser.scala b/parser/src/main/scala/coulomb/parser.scala new file mode 100644 index 000000000..eedcb51d8 --- /dev/null +++ b/parser/src/main/scala/coulomb/parser.scala @@ -0,0 +1,20 @@ +/* + * Copyright 2022 Erik Erlandson + * + * 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 coulomb + +object parser: + val stub = 0 diff --git a/pureconfig/src/main/scala/coulomb/pureconfig.scala b/pureconfig/src/main/scala/coulomb/pureconfig.scala index f27db1509..a2bfa4018 100644 --- a/pureconfig/src/main/scala/coulomb/pureconfig.scala +++ b/pureconfig/src/main/scala/coulomb/pureconfig.scala @@ -22,6 +22,9 @@ import coulomb.{infra => _, *} import coulomb.rational.Rational object pureconfig: + val stub = 0 + +object pureconfig_save: // it would be nice to handle polymorphic inputs // https://github.com/pureconfig/pureconfig/issues/1472 given rationalReader: ConfigReader[Rational] = From 0ac66025df0e9b92e11a0270af4fc1ed79d2b265 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 4 Mar 2023 08:30:34 -0700 Subject: [PATCH 064/141] move &: and TNil to syntax --- core/src/main/scala/coulomb/ops/ops.scala | 3 +- .../coulomb/ops/resolution/standard.scala | 2 +- core/src/main/scala/coulomb/quantity.scala | 5 +++ parser/src/main/scala/coulomb/parser.scala | 39 ++++++++++++++++++- .../runtime/conversion/runtimes/mapping.scala | 26 +++++++------ .../scala/coulomb/ops/resolution/spire.scala | 2 +- 6 files changed, 59 insertions(+), 18 deletions(-) diff --git a/core/src/main/scala/coulomb/ops/ops.scala b/core/src/main/scala/coulomb/ops/ops.scala index 732bd7a7e..406be1c52 100644 --- a/core/src/main/scala/coulomb/ops/ops.scala +++ b/core/src/main/scala/coulomb/ops/ops.scala @@ -170,8 +170,7 @@ object ValuePromotion: import coulomb.infra.meta.typestr - final type &:[H, T] - final type TNil + import coulomb.syntax.typelist.{TNil, &:} transparent inline given ctx_VP_Path[VF, VT]: ValuePromotion[VF, VT] = ${ vpPath[VF, VT] diff --git a/core/src/main/scala/coulomb/ops/resolution/standard.scala b/core/src/main/scala/coulomb/ops/resolution/standard.scala index cbf89b619..ea5d0980f 100644 --- a/core/src/main/scala/coulomb/ops/resolution/standard.scala +++ b/core/src/main/scala/coulomb/ops/resolution/standard.scala @@ -18,7 +18,7 @@ package coulomb.ops.resolution object standard: import coulomb.ops.ValuePromotionPolicy - import coulomb.ops.ValuePromotion.{&:, TNil} + import coulomb.syntax.typelist.{&:, TNil} // ValuePromotion infers the transitive closure of all promotions given ctx_vpp_standard: ValuePromotionPolicy[ diff --git a/core/src/main/scala/coulomb/quantity.scala b/core/src/main/scala/coulomb/quantity.scala index 83910c818..363147bba 100644 --- a/core/src/main/scala/coulomb/quantity.scala +++ b/core/src/main/scala/coulomb/quantity.scala @@ -117,6 +117,11 @@ package syntax { * }}} */ inline def withUnit[U]: Quantity[V, U] = Quantity[U](v) + + package typelist { + final type &:[Head, Tail] + final type TNil + } } /** diff --git a/parser/src/main/scala/coulomb/parser.scala b/parser/src/main/scala/coulomb/parser.scala index eedcb51d8..708263114 100644 --- a/parser/src/main/scala/coulomb/parser.scala +++ b/parser/src/main/scala/coulomb/parser.scala @@ -16,5 +16,40 @@ package coulomb -object parser: - val stub = 0 +import coulomb.RuntimeUnit + +package parser { + abstract class RuntimeUnitParser: + def parse(expr: String): RuntimeUnit + def render(u: RuntimeUnit): String + + object standard: + sealed abstract class RuntimeUnitExprParser extends RuntimeUnitParser: + protected def unames: Map[String, String] + protected def pnames: Set[String] + def parse(expr: String): RuntimeUnit = ??? + def render(u: RuntimeUnit): String = ??? + + object RuntimeUnitExprParser: + inline def of[UTL]: RuntimeUnitExprParser = ${ meta.ofUTL[UTL] } + + object meta: + import scala.quoted.* + import scala.util.{Try, Success, Failure} + import scala.unchecked + import scala.language.implicitConversions + + import coulomb.infra.meta.{*, given} + import coulomb.infra.runtime.meta.{*, given} + + import coulomb.parser.standard.RuntimeUnitExprParser + + def ofUTL[UTL](using Quotes, Type[UTL]): Expr[RuntimeUnitExprParser] = + val un = Map.empty[String, String] + val pn = Set.empty[String] + '{ + new RuntimeUnitExprParser: + protected val unames = ${ Expr(un) } + protected val pnames = ${ Expr(pn) } + } +} diff --git a/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala b/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala index 5a1df279a..7587df32d 100644 --- a/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala +++ b/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala @@ -21,10 +21,11 @@ import scala.collection.immutable.HashMap import coulomb.* import coulomb.rational.Rational -class MappingCoefficientRuntime( - baseUnits: Set[RuntimeUnit.UnitType], - derivedUnits: Map[RuntimeUnit.UnitType, RuntimeUnit] -) extends CoefficientRuntime: +sealed abstract class MappingCoefficientRuntime extends CoefficientRuntime: + // can protected members change and preserve binary compatibility? + protected def base: Set[RuntimeUnit.UnitType] + protected def derived: Map[RuntimeUnit.UnitType, RuntimeUnit] + def coefficientRational( uf: RuntimeUnit, ut: RuntimeUnit @@ -42,10 +43,10 @@ class MappingCoefficientRuntime( case RuntimeUnit.Pow(b, e) => canonical(b).pow(e) case RuntimeUnit.UnitConst(c) => Right(Canonical(c, Canonical.one.sig)) - case u: RuntimeUnit.UnitType if (baseUnits.contains(u)) => + case u: RuntimeUnit.UnitType if (base.contains(u)) => Right(Canonical(Rational.const1, HashMap(u -> Rational.const1))) - case u: RuntimeUnit.UnitType if (derivedUnits.contains(u)) => - canonical(derivedUnits(u)) + case u: RuntimeUnit.UnitType if (derived.contains(u)) => + canonical(derived(u)) case _ => Left(s"canonical: unrecognized unit $u") extension (lhs: Either[String, Canonical]) @@ -57,9 +58,6 @@ extension (lhs: Either[String, Canonical]) lhs.map { l => l.pow(e) } object MappingCoefficientRuntime: - final type &:[H, T] - final type TNil - inline def of[UTL]: MappingCoefficientRuntime = ${ meta.ofUTL[UTL] } case class Canonical(coef: Rational, sig: Map[RuntimeUnit.UnitType, Rational]): @@ -107,12 +105,16 @@ object meta: import coulomb.infra.meta.{*, given} import coulomb.infra.runtime.meta.{*, given} - import MappingCoefficientRuntime.{TNil, &:} + import coulomb.syntax.typelist.{TNil, &:} def ofUTL[UTL](using Quotes, Type[UTL]): Expr[MappingCoefficientRuntime] = import quotes.reflect.* val (bu, du) = utlClosure(typeReprList(TypeRepr.of[UTL])) - '{ new MappingCoefficientRuntime(${ Expr(bu) }, ${ Expr(du) }) } + '{ + new MappingCoefficientRuntime: + protected val base = ${ Expr(bu) } + protected val derived = ${ Expr(du) } + } def utlClosure(using Quotes)( utl: List[quotes.reflect.TypeRepr] diff --git a/spire/src/main/scala/coulomb/ops/resolution/spire.scala b/spire/src/main/scala/coulomb/ops/resolution/spire.scala index 184a9cd68..166b5e2f1 100644 --- a/spire/src/main/scala/coulomb/ops/resolution/spire.scala +++ b/spire/src/main/scala/coulomb/ops/resolution/spire.scala @@ -20,7 +20,7 @@ object spire: import _root_.spire.math.* import coulomb.ops.ValuePromotionPolicy - import coulomb.ops.ValuePromotion.{&:, TNil} + import coulomb.syntax.typelist.{&:, TNil} // ValuePromotion infers the transitive closure of all promotions given ctx_vpp_spire: ValuePromotionPolicy[ From a9ce9993c818fb0946d5129c0e21f7a7d461b516 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 4 Mar 2023 08:44:05 -0700 Subject: [PATCH 065/141] fix typelist import --- runtime/src/test/scala/coulomb/mappingquantity.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/src/test/scala/coulomb/mappingquantity.scala b/runtime/src/test/scala/coulomb/mappingquantity.scala index 9fb422fe2..7a3e92c25 100644 --- a/runtime/src/test/scala/coulomb/mappingquantity.scala +++ b/runtime/src/test/scala/coulomb/mappingquantity.scala @@ -23,7 +23,7 @@ import coulomb.units.us.{*, given} import coulomb.CoefficientRuntime import coulomb.conversion.runtimes.mapping.MappingCoefficientRuntime -import MappingCoefficientRuntime.{TNil, &:} +import coulomb.syntax.typelist.{TNil, &:} val mappingRT: CoefficientRuntime = MappingCoefficientRuntime From 93ef8011069c0b53e730a6345952276cd89d4278 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 4 Mar 2023 10:19:20 -0700 Subject: [PATCH 066/141] factor typeReprList --- core/src/main/scala/coulomb/infra/meta.scala | 16 +++++++++++++++ core/src/main/scala/coulomb/ops/ops.scala | 21 ++++++++++---------- parser/src/main/scala/coulomb/parser.scala | 2 ++ 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/core/src/main/scala/coulomb/infra/meta.scala b/core/src/main/scala/coulomb/infra/meta.scala index da08b27c0..4a126aa96 100644 --- a/core/src/main/scala/coulomb/infra/meta.scala +++ b/core/src/main/scala/coulomb/infra/meta.scala @@ -25,6 +25,8 @@ object meta: import scala.quoted.* import scala.language.implicitConversions + import coulomb.syntax.typelist.{TNil, &:} + given ctx_RationalToExpr: ToExpr[Rational] with def apply(r: Rational)(using Quotes): Expr[Rational] = r match // Rational(1) is a useful special case to have predefined @@ -372,6 +374,20 @@ object meta: case Nil => Nil case (u, e0) :: tail => (u, e0 * e) :: unifyPow(e, tail) + def typeReprList(using Quotes)( + tlist: quotes.reflect.TypeRepr + ): List[quotes.reflect.TypeRepr] = + import quotes.reflect.* + tlist match + case tnil if (tnil =:= TypeRepr.of[TNil]) => Nil + case AppliedType(t, List(head, tail)) if (t =:= TypeRepr.of[&:]) => + head :: typeReprList(tail) + case _ => + report.errorAndAbort( + s"utlClosure: bad type list ${tlist.show}" + ) + null.asInstanceOf[Nothing] + def typestr(using Quotes)(t: quotes.reflect.TypeRepr): String = // The policy goal here is that type aliases are never expanded. typestring(t, false) diff --git a/core/src/main/scala/coulomb/ops/ops.scala b/core/src/main/scala/coulomb/ops/ops.scala index 406be1c52..137a152de 100644 --- a/core/src/main/scala/coulomb/ops/ops.scala +++ b/core/src/main/scala/coulomb/ops/ops.scala @@ -168,7 +168,7 @@ object ValuePromotion: import scala.quoted.* import scala.language.implicitConversions - import coulomb.infra.meta.typestr + import coulomb.infra.meta.* import coulomb.syntax.typelist.{TNil, &:} @@ -205,20 +205,19 @@ object ValuePromotion: iss.tree.tpe.baseType( TypeRepr.of[ValuePromotionPolicy].typeSymbol ): @unchecked - vpp2str(vppt) + vpp2str(typeReprList(vppt)) case _ => report.error("no ValuePromotionPolicy was found in scope") - VppSet.empty[(String, String)] + null.asInstanceOf[Nothing] private def vpp2str(using Quotes)( - vpp: quotes.reflect.TypeRepr + vppl: List[quotes.reflect.TypeRepr] ): VppSet[(String, String)] = import quotes.reflect.* - vpp match - case t if (t =:= TypeRepr.of[TNil]) => - VppSet.empty[(String, String)] - case AppliedType(v, List(AppliedType(t2, List(vf, vt)), tail)) - if ((v =:= TypeRepr.of[&:]) && (t2 =:= TypeRepr.of[Tuple2])) => + vppl match + case Nil => VppSet.empty[(String, String)] + case AppliedType(t2, List(vf, vt)) :: tail + if (t2 =:= TypeRepr.of[Tuple2]) => val vppset = vpp2str(tail) vppset.add( ( @@ -229,9 +228,9 @@ object ValuePromotion: vppset case _ => report.error( - s"type ${typestr(vpp)} is not a valid value promotion policy" + s"type ${typestr(vppl.head)} is not a valid promotion pair" ) - VppSet.empty[(String, String)] + null.asInstanceOf[Nothing] private def pathexists( vf: String, diff --git a/parser/src/main/scala/coulomb/parser.scala b/parser/src/main/scala/coulomb/parser.scala index 708263114..76911f873 100644 --- a/parser/src/main/scala/coulomb/parser.scala +++ b/parser/src/main/scala/coulomb/parser.scala @@ -44,6 +44,8 @@ package parser { import coulomb.parser.standard.RuntimeUnitExprParser + import coulomb.syntax.typelist.{TNil, &:} + def ofUTL[UTL](using Quotes, Type[UTL]): Expr[RuntimeUnitExprParser] = val un = Map.empty[String, String] val pn = Set.empty[String] From 98f6c03cef881577005a8934a51eef58ae013384 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 4 Mar 2023 10:52:13 -0700 Subject: [PATCH 067/141] removed duplicate --- .../runtime/conversion/runtimes/mapping.scala | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala b/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala index 7587df32d..fbbb2ad2c 100644 --- a/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala +++ b/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala @@ -182,20 +182,6 @@ object meta: case derivedunitTR(_) => true case _ => false - def typeReprList(using Quotes)( - tlist: quotes.reflect.TypeRepr - ): List[quotes.reflect.TypeRepr] = - import quotes.reflect.* - tlist match - case tnil if (tnil =:= TypeRepr.of[TNil]) => Nil - case AppliedType(t, List(head, tail)) if (t =:= TypeRepr.of[&:]) => - head :: typeReprList(tail) - case _ => - report.errorAndAbort( - s"utlClosure: bad type list ${tlist.show}" - ) - null.asInstanceOf[Nothing] - private val emptyMap = Map.empty[RuntimeUnit.UnitType, RuntimeUnit] From d1b69e28d51bdb9fb779f9d88642c3065db0d49f Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 4 Mar 2023 15:11:18 -0700 Subject: [PATCH 068/141] baseunitTR --- core/src/main/scala/coulomb/infra/meta.scala | 27 ++++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/core/src/main/scala/coulomb/infra/meta.scala b/core/src/main/scala/coulomb/infra/meta.scala index 4a126aa96..ac917d9df 100644 --- a/core/src/main/scala/coulomb/infra/meta.scala +++ b/core/src/main/scala/coulomb/infra/meta.scala @@ -265,14 +265,9 @@ object meta: object baseunit: def unapply(using Quotes)(u: quotes.reflect.TypeRepr): Boolean = - import quotes.reflect.* - Implicits.search( - TypeRepr - .of[BaseUnit] - .appliedTo(List(u, TypeBounds.empty, TypeBounds.empty)) - ) match - case iss: ImplicitSearchSuccess => true - case _ => false + u match + case baseunitTR(_) => true + case _ => false object derivedunit: def unapply(using qq: Quotes, mode: SigMode)( @@ -291,6 +286,22 @@ object meta: Some(cansig(d)) case _ => None + object baseunitTR: + def unapply(using Quotes)(u: quotes.reflect.TypeRepr): Option[quotes.reflect.TypeRepr] = + import quotes.reflect.* + Implicits.search( + TypeRepr + .of[BaseUnit] + .appliedTo(List(u, TypeBounds.empty, TypeBounds.empty)) + ) match + case iss: ImplicitSearchSuccess => + Some( + iss.tree.tpe.baseType( + TypeRepr.of[BaseUnit].typeSymbol + ) + ) + case _ => None + object derivedunitTR: def unapply(using Quotes)( u: quotes.reflect.TypeRepr From c12e6dd9a6bce44f54983054fbe3a65d9c27677b Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 4 Mar 2023 15:11:50 -0700 Subject: [PATCH 069/141] private utlClosure --- .../scala/coulomb/runtime/conversion/runtimes/mapping.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala b/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala index fbbb2ad2c..ec79573c6 100644 --- a/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala +++ b/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala @@ -116,7 +116,7 @@ object meta: protected val derived = ${ Expr(du) } } - def utlClosure(using Quotes)( + private def utlClosure(using Quotes)( utl: List[quotes.reflect.TypeRepr] ): (Set[RuntimeUnit.UnitType], Map[RuntimeUnit.UnitType, RuntimeUnit]) = import quotes.reflect.* @@ -127,7 +127,7 @@ object meta: val (hbu, hdu) = utClosure(head) (hbu ++ tbu, hdu ++ tdu) - def utClosure(using Quotes)( + private def utClosure(using Quotes)( tr: quotes.reflect.TypeRepr ): (Set[RuntimeUnit.UnitType], Map[RuntimeUnit.UnitType, RuntimeUnit]) = import quotes.reflect.* From a09b742e6454f3933ef3e920beb2a12788cbcb05 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 4 Mar 2023 15:21:44 -0700 Subject: [PATCH 070/141] collect and strset --- parser/src/main/scala/coulomb/parser.scala | 124 ++++++++++++++------- 1 file changed, 86 insertions(+), 38 deletions(-) diff --git a/parser/src/main/scala/coulomb/parser.scala b/parser/src/main/scala/coulomb/parser.scala index 76911f873..4edd3224d 100644 --- a/parser/src/main/scala/coulomb/parser.scala +++ b/parser/src/main/scala/coulomb/parser.scala @@ -14,44 +14,92 @@ * limitations under the License. */ -package coulomb +package coulomb.parser import coulomb.RuntimeUnit -package parser { - abstract class RuntimeUnitParser: - def parse(expr: String): RuntimeUnit - def render(u: RuntimeUnit): String - - object standard: - sealed abstract class RuntimeUnitExprParser extends RuntimeUnitParser: - protected def unames: Map[String, String] - protected def pnames: Set[String] - def parse(expr: String): RuntimeUnit = ??? - def render(u: RuntimeUnit): String = ??? - - object RuntimeUnitExprParser: - inline def of[UTL]: RuntimeUnitExprParser = ${ meta.ofUTL[UTL] } - - object meta: - import scala.quoted.* - import scala.util.{Try, Success, Failure} - import scala.unchecked - import scala.language.implicitConversions - - import coulomb.infra.meta.{*, given} - import coulomb.infra.runtime.meta.{*, given} - - import coulomb.parser.standard.RuntimeUnitExprParser - - import coulomb.syntax.typelist.{TNil, &:} - - def ofUTL[UTL](using Quotes, Type[UTL]): Expr[RuntimeUnitExprParser] = - val un = Map.empty[String, String] - val pn = Set.empty[String] - '{ - new RuntimeUnitExprParser: - protected val unames = ${ Expr(un) } - protected val pnames = ${ Expr(pn) } - } -} +abstract class RuntimeUnitParser: + def parse(expr: String): RuntimeUnit + def render(u: RuntimeUnit): String + +object standard: + sealed abstract class RuntimeUnitExprParser extends RuntimeUnitParser: + protected def unames: Map[String, String] + protected def pnames: Set[String] + def parse(expr: String): RuntimeUnit = + RuntimeUnit.UnitType("no") + def render(u: RuntimeUnit): String = + "no" + + object RuntimeUnitExprParser: + inline def of[UTL]: RuntimeUnitExprParser = ${ meta.ofUTL[UTL] } + +object infra: + import _root_.cats.parse.* + def strset(ss: Set[String]): Parser[String] = + // assumes ss is not empty and all members are length > 0 + // this is guaranteed by construction at compile time + val head = ss.map(_.head) + val po = head.map { c => + val tail = ss.filter(_.head == c).map(_.drop(1)).filter(_.length > 0) + if (tail.isEmpty) + Parser.string(Parser.char(c)) + else + Parser.string(Parser.char(c) ~ strset(tail)) + } + val p = po.drop(1).fold(po.head) { case (p, q) => + Parser.string(p | q) + } + p + +object meta: + import scala.quoted.* + import scala.util.{Try, Success, Failure} + import scala.unchecked + import scala.language.implicitConversions + + import coulomb.infra.meta.{*, given} + import coulomb.infra.runtime.meta.{*, given} + import coulomb.conversion.runtimes.mapping.meta.{*, given} + + import coulomb.parser.standard.RuntimeUnitExprParser + + import coulomb.syntax.typelist.{TNil, &:} + + def ofUTL[UTL](using Quotes, Type[UTL]): Expr[RuntimeUnitExprParser] = + import quotes.reflect.* + val (un, pn) = collect(typeReprList(TypeRepr.of[UTL])) + val pn1 = pn.filter(_.length > 0) + val un1 = un.filter { case (k, _) => + k.length > 0 + } + if (un1.isEmpty) + report.errorAndAbort(s"ofUTL: no defined unit names") + '{ + new RuntimeUnitExprParser: + protected val unames = ${ Expr(un1) } + protected val pnames = ${ Expr(pn1) } + } + + private def collect(using Quotes)(tl: List[quotes.reflect.TypeRepr]): (Map[String, String], Set[String]) = + import quotes.reflect.* + tl match + case Nil => (Map.empty[String, String], Set.empty[String]) + case head :: tail => + val (un, pn) = collect(tail) + head match + case ConstantType(StringConstant(mname)) => + collect(moduleUnits(mname)) + case baseunitTR(tr) => + val AppliedType(_, List(_, n, _)) = tr: @unchecked + val ConstantType(StringConstant(name)) = n: @unchecked + (un + (name -> head.typeSymbol.fullName), pn) + case derivedunitTR(tr) => + val AppliedType(_, List(_, _, n, _)) = tr: @unchecked + val ConstantType(StringConstant(name)) = n: @unchecked + val unr = un + (name -> head.typeSymbol.fullName) + val pnr = if (convertible(head, TypeRepr.of[1])) pn + name else pn + (unr, pnr) + case _ => + report.errorAndAbort(s"collect: bad type ${head.show}") + null.asInstanceOf[Nothing] From f7287c9d18f337929c5c6b49ed3a5f2e2ddc59de Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 4 Mar 2023 15:24:38 -0700 Subject: [PATCH 071/141] format --- core/src/main/scala/coulomb/infra/meta.scala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/src/main/scala/coulomb/infra/meta.scala b/core/src/main/scala/coulomb/infra/meta.scala index ac917d9df..086005f4a 100644 --- a/core/src/main/scala/coulomb/infra/meta.scala +++ b/core/src/main/scala/coulomb/infra/meta.scala @@ -267,7 +267,7 @@ object meta: def unapply(using Quotes)(u: quotes.reflect.TypeRepr): Boolean = u match case baseunitTR(_) => true - case _ => false + case _ => false object derivedunit: def unapply(using qq: Quotes, mode: SigMode)( @@ -287,7 +287,9 @@ object meta: case _ => None object baseunitTR: - def unapply(using Quotes)(u: quotes.reflect.TypeRepr): Option[quotes.reflect.TypeRepr] = + def unapply(using Quotes)( + u: quotes.reflect.TypeRepr + ): Option[quotes.reflect.TypeRepr] = import quotes.reflect.* Implicits.search( TypeRepr From d70bf5849bcfe3c2e8dd77e1dd80fd5fac049773 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 5 Mar 2023 06:24:45 -0700 Subject: [PATCH 072/141] optimze with void --- parser/src/main/scala/coulomb/parser.scala | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/parser/src/main/scala/coulomb/parser.scala b/parser/src/main/scala/coulomb/parser.scala index 4edd3224d..757170829 100644 --- a/parser/src/main/scala/coulomb/parser.scala +++ b/parser/src/main/scala/coulomb/parser.scala @@ -37,18 +37,21 @@ object standard: object infra: import _root_.cats.parse.* def strset(ss: Set[String]): Parser[String] = + strsetvoid(ss).string + + private def strsetvoid(ss: Set[String]): Parser[Unit] = // assumes ss is not empty and all members are length > 0 // this is guaranteed by construction at compile time val head = ss.map(_.head) val po = head.map { c => val tail = ss.filter(_.head == c).map(_.drop(1)).filter(_.length > 0) if (tail.isEmpty) - Parser.string(Parser.char(c)) + Parser.char(c) else - Parser.string(Parser.char(c) ~ strset(tail)) + (Parser.char(c) ~ strsetvoid(tail)).void } val p = po.drop(1).fold(po.head) { case (p, q) => - Parser.string(p | q) + (p | q).void } p From bc6d01effb850eed271971849f575fc76e794469 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 5 Mar 2023 06:33:54 -0700 Subject: [PATCH 073/141] foldLeft --- parser/src/main/scala/coulomb/parser.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parser/src/main/scala/coulomb/parser.scala b/parser/src/main/scala/coulomb/parser.scala index 757170829..2d2f9f443 100644 --- a/parser/src/main/scala/coulomb/parser.scala +++ b/parser/src/main/scala/coulomb/parser.scala @@ -50,7 +50,7 @@ object infra: else (Parser.char(c) ~ strsetvoid(tail)).void } - val p = po.drop(1).fold(po.head) { case (p, q) => + val p = po.drop(1).foldLeft(po.head) { case (p, q) => (p | q).void } p From ee79a2f16e6c1d888873bea86588c41416427cce Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 5 Mar 2023 07:35:59 -0700 Subject: [PATCH 074/141] parse named units --- parser/src/main/scala/coulomb/parser.scala | 52 +++++++++++++++++----- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/parser/src/main/scala/coulomb/parser.scala b/parser/src/main/scala/coulomb/parser.scala index 2d2f9f443..f75e55654 100644 --- a/parser/src/main/scala/coulomb/parser.scala +++ b/parser/src/main/scala/coulomb/parser.scala @@ -36,24 +36,50 @@ object standard: object infra: import _root_.cats.parse.* + + def named(unames: Map[String, String], pnames: Set[String]): Parser[RuntimeUnit] = + // unames is never empty by construction + val unit = strset(unames.keySet).map { name => + // map will always succeed because only names in unames can parse + RuntimeUnit.UnitType(unames(name)) + } + if (pnames.isEmpty) + // if there are no prefix units, just parse units + unit + else + val prefix = strset(pnames).map { name => + // prefixes are also defined in unames + RuntimeUnit.UnitType(unames(name)) + } + val pfu = (prefix ~ unit).map { case (lhs, rhs) => + // => prefix * unit + RuntimeUnit.Mul(lhs, rhs) + } + // parse either or + // test pfu first + pfu | unit + def strset(ss: Set[String]): Parser[String] = strsetvoid(ss).string + // assumes ss is not empty and all members are length > 0 + // this is guaranteed by construction at compile time private def strsetvoid(ss: Set[String]): Parser[Unit] = - // assumes ss is not empty and all members are length > 0 - // this is guaranteed by construction at compile time - val head = ss.map(_.head) - val po = head.map { c => - val tail = ss.filter(_.head == c).map(_.drop(1)).filter(_.length > 0) - if (tail.isEmpty) - Parser.char(c) + // construct a parser "branch" for each starting character + val hp = ss.map(_.head).map { h => + // set of string tails starting with char h + val tails = ss.filter(_.head == h).map(_.drop(1)).filter(_.length > 0) + if (tails.isEmpty) + // no remaining string tails, just parse char h + Parser.char(h) else - (Parser.char(c) ~ strsetvoid(tail)).void + // parse h followed by parser for tails + (Parser.char(h) ~ strsetvoid(tails)).void } - val p = po.drop(1).foldLeft(po.head) { case (p, q) => + // final parser is just "or" of branches: hp(0) | hp(1) | hp(2) ... + hp.drop(1).foldLeft(hp.head) { case (p, q) => (p | q).void } - p object meta: import scala.quoted.* @@ -72,11 +98,13 @@ object meta: def ofUTL[UTL](using Quotes, Type[UTL]): Expr[RuntimeUnitExprParser] = import quotes.reflect.* val (un, pn) = collect(typeReprList(TypeRepr.of[UTL])) + // remove any unit names that are empty strings val pn1 = pn.filter(_.length > 0) val un1 = un.filter { case (k, _) => k.length > 0 } if (un1.isEmpty) + // unit map must be non-empty report.errorAndAbort(s"ofUTL: no defined unit names") '{ new RuntimeUnitExprParser: @@ -96,11 +124,15 @@ object meta: case baseunitTR(tr) => val AppliedType(_, List(_, n, _)) = tr: @unchecked val ConstantType(StringConstant(name)) = n: @unchecked + // base units are never prefix units because prefix units are + // derived from '1' (unitless) (un + (name -> head.typeSymbol.fullName), pn) case derivedunitTR(tr) => val AppliedType(_, List(_, _, n, _)) = tr: @unchecked val ConstantType(StringConstant(name)) = n: @unchecked + // always add to unit types val unr = un + (name -> head.typeSymbol.fullName) + // if it is derived from unitless, also add it to prefix unit set val pnr = if (convertible(head, TypeRepr.of[1])) pn + name else pn (unr, pnr) case _ => From 20205cac7a7dc4acdd6da92539b6d4be8ca44efe Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 5 Mar 2023 07:41:16 -0700 Subject: [PATCH 075/141] parse returns Either --- parser/src/main/scala/coulomb/parser.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/parser/src/main/scala/coulomb/parser.scala b/parser/src/main/scala/coulomb/parser.scala index f75e55654..eb2a77f2e 100644 --- a/parser/src/main/scala/coulomb/parser.scala +++ b/parser/src/main/scala/coulomb/parser.scala @@ -19,15 +19,15 @@ package coulomb.parser import coulomb.RuntimeUnit abstract class RuntimeUnitParser: - def parse(expr: String): RuntimeUnit + def parse(expr: String): Either[String, RuntimeUnit] def render(u: RuntimeUnit): String object standard: sealed abstract class RuntimeUnitExprParser extends RuntimeUnitParser: protected def unames: Map[String, String] protected def pnames: Set[String] - def parse(expr: String): RuntimeUnit = - RuntimeUnit.UnitType("no") + def parse(expr: String): Either[String, RuntimeUnit] = + Left("no") def render(u: RuntimeUnit): String = "no" From f2eb856df35fcada94fcb903e461710638f655f2 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 5 Mar 2023 08:45:52 -0700 Subject: [PATCH 076/141] oneOf --- parser/src/main/scala/coulomb/parser.scala | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/parser/src/main/scala/coulomb/parser.scala b/parser/src/main/scala/coulomb/parser.scala index eb2a77f2e..367f81ca1 100644 --- a/parser/src/main/scala/coulomb/parser.scala +++ b/parser/src/main/scala/coulomb/parser.scala @@ -66,7 +66,7 @@ object infra: // this is guaranteed by construction at compile time private def strsetvoid(ss: Set[String]): Parser[Unit] = // construct a parser "branch" for each starting character - val hp = ss.map(_.head).map { h => + val hp = ss.map(_.head).toList.map { h => // set of string tails starting with char h val tails = ss.filter(_.head == h).map(_.drop(1)).filter(_.length > 0) if (tails.isEmpty) @@ -77,9 +77,7 @@ object infra: (Parser.char(h) ~ strsetvoid(tails)).void } // final parser is just "or" of branches: hp(0) | hp(1) | hp(2) ... - hp.drop(1).foldLeft(hp.head) { case (p, q) => - (p | q).void - } + Parser.oneOf(hp) object meta: import scala.quoted.* From 57ad68e07e202cac244e1a630a166a6e11c428ac Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 5 Mar 2023 11:03:59 -0700 Subject: [PATCH 077/141] mul not working --- parser/src/main/scala/coulomb/parser.scala | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/parser/src/main/scala/coulomb/parser.scala b/parser/src/main/scala/coulomb/parser.scala index 367f81ca1..757a5da41 100644 --- a/parser/src/main/scala/coulomb/parser.scala +++ b/parser/src/main/scala/coulomb/parser.scala @@ -37,6 +37,19 @@ object standard: object infra: import _root_.cats.parse.* + val ws: Parser[Unit] = Parser.charIn(" \t\r\n").void + val ws0: Parser0[Unit] = ws.rep0.void + + def unit(named: Parser[RuntimeUnit]): Parser[RuntimeUnit] = + Parser.recursive[RuntimeUnit] { recurse => + val sub: Parser[RuntimeUnit] = recurse.between(Parser.char('(') <* ws0, ws0 *> Parser.char(')')) + val mul: Parser[RuntimeUnit] = + ((recurse <* Parser.char('*').soft.surroundedBy(ws0)).soft ~ recurse).map { case (lhs, rhs) => + RuntimeUnit.Mul(lhs, rhs) + } + Parser.oneOf(named :: sub :: mul :: Nil) + } + def named(unames: Map[String, String], pnames: Set[String]): Parser[RuntimeUnit] = // unames is never empty by construction val unit = strset(unames.keySet).map { name => From 010ae7180d015abf01e4c8ec64107b00eaa51bcc Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Tue, 7 Mar 2023 14:58:08 -0700 Subject: [PATCH 078/141] play with chainl1 (not working) --- parser/src/main/scala/coulomb/parser.scala | 58 +++++++++++++++++++--- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/parser/src/main/scala/coulomb/parser.scala b/parser/src/main/scala/coulomb/parser.scala index 757a5da41..1861e2dd4 100644 --- a/parser/src/main/scala/coulomb/parser.scala +++ b/parser/src/main/scala/coulomb/parser.scala @@ -40,15 +40,61 @@ object infra: val ws: Parser[Unit] = Parser.charIn(" \t\r\n").void val ws0: Parser0[Unit] = ws.rep0.void - def unit(named: Parser[RuntimeUnit]): Parser[RuntimeUnit] = + def unit_nope(named: Parser[RuntimeUnit]): Parser[RuntimeUnit] = Parser.recursive[RuntimeUnit] { recurse => - val sub: Parser[RuntimeUnit] = recurse.between(Parser.char('(') <* ws0, ws0 *> Parser.char(')')) + // ( ) + val sub: Parser[RuntimeUnit] = + recurse.between(Parser.char('(') <* ws0, Parser.char(')') <* ws0) + + // * val mul: Parser[RuntimeUnit] = - ((recurse <* Parser.char('*').soft.surroundedBy(ws0)).soft ~ recurse).map { case (lhs, rhs) => - RuntimeUnit.Mul(lhs, rhs) - } - Parser.oneOf(named :: sub :: mul :: Nil) + chainl1(recurse, (Parser.char('*') <* ws0).as(RuntimeUnit.Mul(_, _))) + + ws0.with1 *> Parser.oneOf((named <* ws0) :: sub :: mul :: Nil) <* Parser.end + } + + def unit(named: Parser[RuntimeUnit]): Parser[RuntimeUnit] = + lazy val expr: Parser[RuntimeUnit] = Parser.defer { + lazy val mul: Parser[RuntimeUnit] = + chainl1(term, (Parser.char('*') <* ws0).as(RuntimeUnit.Mul(_, _))) + lazy val term: Parser[RuntimeUnit] = + paren | (named <* ws0) + lazy val paren: Parser[RuntimeUnit] = + expr.between(Parser.char('(') <* ws0, Parser.char(')') <* ws0) + mul + } + ws0.with1 *> expr <* Parser.end + + def chainl1[X](p: Parser[X], op: Parser[(X, X) => X]): Parser[X] = + lazy val rest: Parser0[X => X] = Parser.defer0 { + val some: Parser0[X => X] = (op, p, rest).mapN { + // found an , with possibly more + (f, y, next) => ((x: X) => next(f(x, y))) + } + // none consumes no input + val none: Parser0[X => X] = Parser.pure(identity[X]) + some | none } + // this feels wrong but .with1 returns With1, not Parser + rapp(p, rest).asInstanceOf[Parser[X]] + + // parsley <*> + def app[X, Z](f: Parser0[X => Z], x: Parser0[X]): Parser0[Z] = + (f ~ x).map { case (f, x) => f(x) } + + // parsley <**> + def rapp[X, Z](x: Parser0[X], f: Parser0[X => Z]): Parser0[Z] = + (x ~ f).map { case (x, f) => f(x) } + + // parsley zipped + // can scala 3 '*:' clean this up? + extension [X1, X2](p: (Parser0[X1], Parser0[X2])) + def mapN[Z](f: (X1, X2) => Z): Parser0[Z] = + (p._1 ~ p._2).map { case (x1, x2) => f(x1, x2) } + + extension [X1, X2, X3](p: (Parser0[X1], Parser0[X2], Parser0[X3])) + def mapN[Z](f: (X1, X2, X3) => Z): Parser0[Z] = + ((p._1 ~ p._2) ~ p._3).map { case ((x1, x2), x3) => f(x1, x2, x3) } def named(unames: Map[String, String], pnames: Set[String]): Parser[RuntimeUnit] = // unames is never empty by construction From 07b7446f6e4156fcb0c5d7427b17c34988f12fd9 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Wed, 8 Mar 2023 07:47:18 -0700 Subject: [PATCH 079/141] chainl1 and named working correctly --- parser/src/main/scala/coulomb/parser.scala | 65 ++++++++++------------ 1 file changed, 30 insertions(+), 35 deletions(-) diff --git a/parser/src/main/scala/coulomb/parser.scala b/parser/src/main/scala/coulomb/parser.scala index 1861e2dd4..3b168bdd9 100644 --- a/parser/src/main/scala/coulomb/parser.scala +++ b/parser/src/main/scala/coulomb/parser.scala @@ -40,28 +40,18 @@ object infra: val ws: Parser[Unit] = Parser.charIn(" \t\r\n").void val ws0: Parser0[Unit] = ws.rep0.void - def unit_nope(named: Parser[RuntimeUnit]): Parser[RuntimeUnit] = - Parser.recursive[RuntimeUnit] { recurse => - // ( ) - val sub: Parser[RuntimeUnit] = - recurse.between(Parser.char('(') <* ws0, Parser.char(')') <* ws0) - - // * - val mul: Parser[RuntimeUnit] = - chainl1(recurse, (Parser.char('*') <* ws0).as(RuntimeUnit.Mul(_, _))) - - ws0.with1 *> Parser.oneOf((named <* ws0) :: sub :: mul :: Nil) <* Parser.end - } - def unit(named: Parser[RuntimeUnit]): Parser[RuntimeUnit] = lazy val expr: Parser[RuntimeUnit] = Parser.defer { - lazy val mul: Parser[RuntimeUnit] = - chainl1(term, (Parser.char('*') <* ws0).as(RuntimeUnit.Mul(_, _))) + val termops: Parser[(RuntimeUnit, RuntimeUnit) => RuntimeUnit] = + (Parser.char('*') <* ws0).as(RuntimeUnit.Mul(_, _)) | + (Parser.char('/') <* ws0).as(RuntimeUnit.Div(_, _)) lazy val term: Parser[RuntimeUnit] = + chainl1(atom, termops) + lazy val atom: Parser[RuntimeUnit] = paren | (named <* ws0) lazy val paren: Parser[RuntimeUnit] = expr.between(Parser.char('(') <* ws0, Parser.char(')') <* ws0) - mul + term } ws0.with1 *> expr <* Parser.end @@ -96,27 +86,30 @@ object infra: def mapN[Z](f: (X1, X2, X3) => Z): Parser0[Z] = ((p._1 ~ p._2) ~ p._3).map { case ((x1, x2), x3) => f(x1, x2, x3) } + def unitname: Parser[String] = + // this might be extended but not until I have a reason and a principle + // one possible extension would be "any printable char not in { '(', ')', '*', etc }" + // however I'm not sure if there is an efficient way to express that + // (starting char can also not be digit, + or -) + Parser.charIn('a' to 'z').rep.string + def named(unames: Map[String, String], pnames: Set[String]): Parser[RuntimeUnit] = - // unames is never empty by construction - val unit = strset(unames.keySet).map { name => - // map will always succeed because only names in unames can parse - RuntimeUnit.UnitType(unames(name)) + val prefixunit = (strset(pnames) ~ strset(unames.keySet `diff` pnames)) <* Parser.end + unitname.flatMap { name => + if (unames.contains(name)) + // name is a defined unit, return its type + Parser.pure(RuntimeUnit.UnitType(unames(name))) + else + // otherwise see if it can be parsed as + prefixunit.parse(name) match + case Right((_, (pn, un))) => + // => * + val p = RuntimeUnit.UnitType(unames(pn)) + val u = RuntimeUnit.UnitType(unames(un)) + Parser.pure(RuntimeUnit.Mul(p, u)) + case Left(_) => + Parser.failWith[RuntimeUnit](s"unrecognized unit '$name'") } - if (pnames.isEmpty) - // if there are no prefix units, just parse units - unit - else - val prefix = strset(pnames).map { name => - // prefixes are also defined in unames - RuntimeUnit.UnitType(unames(name)) - } - val pfu = (prefix ~ unit).map { case (lhs, rhs) => - // => prefix * unit - RuntimeUnit.Mul(lhs, rhs) - } - // parse either or - // test pfu first - pfu | unit def strset(ss: Set[String]): Parser[String] = strsetvoid(ss).string @@ -136,6 +129,8 @@ object infra: (Parser.char(h) ~ strsetvoid(tails)).void } // final parser is just "or" of branches: hp(0) | hp(1) | hp(2) ... + // these are safe to "or" because by construction they share + // no common left factor Parser.oneOf(hp) object meta: From cd783b3509994e4da622843713e2397186468ccb Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Wed, 8 Mar 2023 13:35:36 -0700 Subject: [PATCH 080/141] complete unit expression grammar --- parser/src/main/scala/coulomb/parser.scala | 208 ++++++++++++++++----- 1 file changed, 164 insertions(+), 44 deletions(-) diff --git a/parser/src/main/scala/coulomb/parser.scala b/parser/src/main/scala/coulomb/parser.scala index 3b168bdd9..8a4756bd3 100644 --- a/parser/src/main/scala/coulomb/parser.scala +++ b/parser/src/main/scala/coulomb/parser.scala @@ -16,7 +16,10 @@ package coulomb.parser +import scala.util.{Try, Success, Failure} + import coulomb.RuntimeUnit +import coulomb.rational.Rational abstract class RuntimeUnitParser: def parse(expr: String): Either[String, RuntimeUnit] @@ -37,65 +40,106 @@ object standard: object infra: import _root_.cats.parse.* + // for consuming whitespace val ws: Parser[Unit] = Parser.charIn(" \t\r\n").void val ws0: Parser0[Unit] = ws.rep0.void - def unit(named: Parser[RuntimeUnit]): Parser[RuntimeUnit] = - lazy val expr: Parser[RuntimeUnit] = Parser.defer { - val termops: Parser[(RuntimeUnit, RuntimeUnit) => RuntimeUnit] = - (Parser.char('*') <* ws0).as(RuntimeUnit.Mul(_, _)) | - (Parser.char('/') <* ws0).as(RuntimeUnit.Div(_, _)) - lazy val term: Parser[RuntimeUnit] = - chainl1(atom, termops) - lazy val atom: Parser[RuntimeUnit] = - paren | (named <* ws0) - lazy val paren: Parser[RuntimeUnit] = - expr.between(Parser.char('(') <* ws0, Parser.char(')') <* ws0) - term - } - ws0.with1 *> expr <* Parser.end - - def chainl1[X](p: Parser[X], op: Parser[(X, X) => X]): Parser[X] = - lazy val rest: Parser0[X => X] = Parser.defer0 { - val some: Parser0[X => X] = (op, p, rest).mapN { - // found an , with possibly more - (f, y, next) => ((x: X) => next(f(x, y))) - } - // none consumes no input - val none: Parser0[X => X] = Parser.pure(identity[X]) - some | none + // numeric literals parse into UnitConst objects + val numlit: Parser[RuntimeUnit] = + cats.parse.Numbers.jsonNumber.flatMap { lit => + lit match + case intlit(v) => + Parser.pure(RuntimeUnit.UnitConst(Rational(v, 1))) + case fplit(v) => + Parser.pure(RuntimeUnit.UnitConst(Rational(v))) + case _ => + Parser.failWith[RuntimeUnit](s"bad numeric literal '$lit'") } - // this feels wrong but .with1 returns With1, not Parser - rapp(p, rest).asInstanceOf[Parser[X]] - - // parsley <*> - def app[X, Z](f: Parser0[X => Z], x: Parser0[X]): Parser0[Z] = - (f ~ x).map { case (f, x) => f(x) } - - // parsley <**> - def rapp[X, Z](x: Parser0[X], f: Parser0[X => Z]): Parser0[Z] = - (x ~ f).map { case (x, f) => f(x) } - // parsley zipped - // can scala 3 '*:' clean this up? - extension [X1, X2](p: (Parser0[X1], Parser0[X2])) - def mapN[Z](f: (X1, X2) => Z): Parser0[Z] = - (p._1 ~ p._2).map { case (x1, x2) => f(x1, x2) } + object intlit: + def unapply(lit: String): Option[BigInt] = + Try { BigInt(lit) }.toOption - extension [X1, X2, X3](p: (Parser0[X1], Parser0[X2], Parser0[X3])) - def mapN[Z](f: (X1, X2, X3) => Z): Parser0[Z] = - ((p._1 ~ p._2) ~ p._3).map { case ((x1, x2), x3) => f(x1, x2, x3) } + object fplit: + def unapply(lit: String): Option[Double] = + Try { lit.toDouble }.toOption - def unitname: Parser[String] = + // a token representing a unit name literal + // examples: "meter", "second", etc + // note that for any defined prefix and unit, is also valid + // for example if "kilo" and "meter" are defined units, "kilometer" will also + // parse correctly as RuntimeUnit.Mul(Kilo, Meter) + val unitlit: Parser[String] = // this might be extended but not until I have a reason and a principle // one possible extension would be "any printable char not in { '(', ')', '*', etc }" // however I'm not sure if there is an efficient way to express that // (starting char can also not be digit, + or -) Parser.charIn('a' to 'z').rep.string + // used for left-factoring the parsing for sequences of mul and div + val muldivop: Parser[(RuntimeUnit, RuntimeUnit) => RuntimeUnit] = + (Parser.char('*') <* ws0).as(RuntimeUnit.Mul(_, _)) | + (Parser.char('/') <* ws0).as(RuntimeUnit.Div(_, _)) + + // used for left-factoring the parsing of "^" (power) + val powop: Parser[(RuntimeUnit, RuntimeUnit) => RuntimeUnit] = + (Parser.char('^') <* ws0).as { (b: RuntimeUnit, e: RuntimeUnit) => + // we do not have to check for Left value of 'evalnum' here + // because it is verified during parsing + RuntimeUnit.Pow(b, e.evalnum.toSeq.head) + } + + def unit(named: Parser[RuntimeUnit]): Parser[RuntimeUnit] = + lazy val unitexpr: Parser[RuntimeUnit] = Parser.defer { + // sequence of mul and div operators + // these have lowest precedence and form the top of the parse tree + // example: * / * ... + lazy val muldiv: Parser[RuntimeUnit] = + chainl1(pow, muldivop) + + // powers of form ^ , where: + // may be any unit expression and + // is an expression that must evaluate to a valid numeric constant + lazy val pow: Parser[RuntimeUnit] = + binaryl1(atom, numeric, powop) + + // numeric literal, named unit, or sub-expr in parens + lazy val atom: Parser[RuntimeUnit] = + paren | (numlit <* ws0) | (named <* ws0) + + // any unit subexpression inside of parens: () + lazy val paren: Parser[RuntimeUnit] = + unitexpr.between(Parser.char('(') <* ws0, Parser.char(')') <* ws0) + + // parses a RuntimeUnit expression, but only succeeds + // if it evaluates to a constant numeric value + // note this is based on 'atom' so non-atomic expressions may only + // appear inside of () + // used to enforce that exponent of powers is a valid numeric value + lazy val numeric: Parser[RuntimeUnit] = + atom.flatMap { u => + u.evalnum match + case Right(v) => + Parser.pure(RuntimeUnit.UnitConst(v)) + case Left(e) => + Parser.failWith[RuntimeUnit](e) + } + + // return the top of the parse tree + muldiv + } + // parse a unit expression, consuming any leading whitespace + // and requiring parsing reach end of input + // (trailing whitespace is consumed inside unitexpr) + ws0.with1 *> unitexpr <* Parser.end + + // parses "raw" unit literals - only succeeds if the literal is + // in the list of defined units (or unit prefixes) + // these lists are intended to be constructed at compile-time via scala metaprogramming + // to reduce errors def named(unames: Map[String, String], pnames: Set[String]): Parser[RuntimeUnit] = val prefixunit = (strset(pnames) ~ strset(unames.keySet `diff` pnames)) <* Parser.end - unitname.flatMap { name => + unitlit.flatMap { name => if (unames.contains(name)) // name is a defined unit, return its type Parser.pure(RuntimeUnit.UnitType(unames(name))) @@ -133,6 +177,82 @@ object infra: // no common left factor Parser.oneOf(hp) + extension (u: RuntimeUnit) + // attempt to evaluate a RuntimeUnit into a valid + // numeric constant value. This is used to constrain + // specialized parsing of RuntimeUnit expressions that + // are required to represent numeric constants + def evalnum: Either[String, Rational] = + u match + case RuntimeUnit.UnitConst(v) => Right(v) + case RuntimeUnit.Mul(lhs, rhs) => + for { + lv <- lhs.evalnum + rv <- rhs.evalnum + } yield (lv * rv) + case RuntimeUnit.Div(num, den) => + den.evalnum match + case Left(e) => Left(e) + case Right(dv) => + if (dv == Rational.const0) + Left("div by zero") + else + for { + nv <- num.evalnum + } yield (nv / dv) + case RuntimeUnit.Pow(b, e) => + for { + bv <- b.evalnum + } yield bv.pow(e) + case _ => + Left(s"bad numeric expression: $u") + + // the following are combinators for factoring left-recursive grammars + // they are taken from this paper: + // https://github.com/j-mie6/design-patterns-for-parser-combinators#readme + def chainl1[X](p: Parser[X], op: Parser[(X, X) => X]): Parser[X] = + lazy val rest: Parser0[X => X] = Parser.defer0 { + val some: Parser0[X => X] = (op, p, rest).mapN { + // found an , with possibly more + (f, y, next) => ((x: X) => next(f(x, y))) + } + // none consumes no input + val none: Parser0[X => X] = Parser.pure(identity[X]) + // "some" expected to be distinguished by leading char of "op" + // for example lhs + rhs distinguished by '+' + some | none + } + // this feels wrong but .with1 returns With1, not Parser + rapp(p, rest).asInstanceOf[Parser[X]] + + // like chainl1 but specifically a single left-factored binary expr + // [ ] + def binaryl1[X](pl: Parser[X], pr: Parser[X], op: Parser[(X, X) => X]): Parser[X] = + val some: Parser0[X => X] = (op, pr).mapN { + // found an + (f, y) => ((x: X) => f(x, y)) + } + val none: Parser0[X => X] = Parser.pure(identity[X]) + rapp(pl, some | none).asInstanceOf[Parser[X]] + + // parsley <*> + def app[X, Z](f: Parser0[X => Z], x: Parser0[X]): Parser0[Z] = + (f ~ x).map { case (f, x) => f(x) } + + // parsley <**> + def rapp[X, Z](x: Parser0[X], f: Parser0[X => Z]): Parser0[Z] = + (x ~ f).map { case (x, f) => f(x) } + + // parsley zipped + // can scala 3 '*:' clean this up? + extension [X1, X2](p: (Parser0[X1], Parser0[X2])) + def mapN[Z](f: (X1, X2) => Z): Parser0[Z] = + (p._1 ~ p._2).map { case (x1, x2) => f(x1, x2) } + + extension [X1, X2, X3](p: (Parser0[X1], Parser0[X2], Parser0[X3])) + def mapN[Z](f: (X1, X2, X3) => Z): Parser0[Z] = + ((p._1 ~ p._2) ~ p._3).map { case ((x1, x2), x3) => f(x1, x2, x3) } + object meta: import scala.quoted.* import scala.util.{Try, Success, Failure} From 1474272e5ac39c780210c516aa878f566ab3ee25 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Fri, 10 Mar 2023 16:58:32 -0700 Subject: [PATCH 081/141] replace &: with scala *: --- core/src/main/scala/coulomb/infra/meta.scala | 8 +++--- core/src/main/scala/coulomb/ops/ops.scala | 6 ++--- .../coulomb/ops/resolution/standard.scala | 3 +-- core/src/main/scala/coulomb/quantity.scala | 5 ---- parser/src/main/scala/coulomb/parser.scala | 27 ++++++++++--------- .../runtime/conversion/runtimes/mapping.scala | 4 +-- .../test/scala/coulomb/mappingquantity.scala | 4 +-- .../scala/coulomb/ops/resolution/spire.scala | 9 +++---- 8 files changed, 26 insertions(+), 40 deletions(-) diff --git a/core/src/main/scala/coulomb/infra/meta.scala b/core/src/main/scala/coulomb/infra/meta.scala index 086005f4a..8659f597b 100644 --- a/core/src/main/scala/coulomb/infra/meta.scala +++ b/core/src/main/scala/coulomb/infra/meta.scala @@ -25,8 +25,6 @@ object meta: import scala.quoted.* import scala.language.implicitConversions - import coulomb.syntax.typelist.{TNil, &:} - given ctx_RationalToExpr: ToExpr[Rational] with def apply(r: Rational)(using Quotes): Expr[Rational] = r match // Rational(1) is a useful special case to have predefined @@ -392,12 +390,12 @@ object meta: ): List[quotes.reflect.TypeRepr] = import quotes.reflect.* tlist match - case tnil if (tnil =:= TypeRepr.of[TNil]) => Nil - case AppliedType(t, List(head, tail)) if (t =:= TypeRepr.of[&:]) => + case tnil if (tnil =:= TypeRepr.of[EmptyTuple]) => Nil + case AppliedType(t, List(head, tail)) if (t =:= TypeRepr.of[*:]) => head :: typeReprList(tail) case _ => report.errorAndAbort( - s"utlClosure: bad type list ${tlist.show}" + s"typeReprList: bad type list ${tlist.show}" ) null.asInstanceOf[Nothing] diff --git a/core/src/main/scala/coulomb/ops/ops.scala b/core/src/main/scala/coulomb/ops/ops.scala index 137a152de..b63356756 100644 --- a/core/src/main/scala/coulomb/ops/ops.scala +++ b/core/src/main/scala/coulomb/ops/ops.scala @@ -170,8 +170,6 @@ object ValuePromotion: import coulomb.infra.meta.* - import coulomb.syntax.typelist.{TNil, &:} - transparent inline given ctx_VP_Path[VF, VT]: ValuePromotion[VF, VT] = ${ vpPath[VF, VT] } @@ -255,9 +253,9 @@ object ValuePromotion: done = true haspath -final class ValuePromotionPolicy[Pairs] +final class ValuePromotionPolicy[Pairs <: Tuple] object ValuePromotionPolicy: - def apply[P](): ValuePromotionPolicy[P] = new ValuePromotionPolicy[P] + def apply[P <: Tuple](): ValuePromotionPolicy[P] = new ValuePromotionPolicy[P] final case class ShowUnit[U](value: String) object ShowUnit: diff --git a/core/src/main/scala/coulomb/ops/resolution/standard.scala b/core/src/main/scala/coulomb/ops/resolution/standard.scala index ea5d0980f..6338e25a7 100644 --- a/core/src/main/scala/coulomb/ops/resolution/standard.scala +++ b/core/src/main/scala/coulomb/ops/resolution/standard.scala @@ -18,9 +18,8 @@ package coulomb.ops.resolution object standard: import coulomb.ops.ValuePromotionPolicy - import coulomb.syntax.typelist.{&:, TNil} // ValuePromotion infers the transitive closure of all promotions given ctx_vpp_standard: ValuePromotionPolicy[ - (Int, Long) &: (Long, Float) &: (Float, Double) &: TNil + (Int, Long) *: (Long, Float) *: (Float, Double) *: EmptyTuple ] = ValuePromotionPolicy() diff --git a/core/src/main/scala/coulomb/quantity.scala b/core/src/main/scala/coulomb/quantity.scala index 363147bba..83910c818 100644 --- a/core/src/main/scala/coulomb/quantity.scala +++ b/core/src/main/scala/coulomb/quantity.scala @@ -117,11 +117,6 @@ package syntax { * }}} */ inline def withUnit[U]: Quantity[V, U] = Quantity[U](v) - - package typelist { - final type &:[Head, Tail] - final type TNil - } } /** diff --git a/parser/src/main/scala/coulomb/parser.scala b/parser/src/main/scala/coulomb/parser.scala index 8a4756bd3..330b296a5 100644 --- a/parser/src/main/scala/coulomb/parser.scala +++ b/parser/src/main/scala/coulomb/parser.scala @@ -27,15 +27,16 @@ abstract class RuntimeUnitParser: object standard: sealed abstract class RuntimeUnitExprParser extends RuntimeUnitParser: - protected def unames: Map[String, String] - protected def pnames: Set[String] + def unames: Map[String, String] + def pnames: Set[String] def parse(expr: String): Either[String, RuntimeUnit] = Left("no") def render(u: RuntimeUnit): String = "no" object RuntimeUnitExprParser: - inline def of[UTL]: RuntimeUnitExprParser = ${ meta.ofUTL[UTL] } + inline def of[UTL <: Tuple]: RuntimeUnitExprParser = + ${ coulomb.parser.meta.ofUTL[UTL] } object infra: import _root_.cats.parse.* @@ -138,7 +139,11 @@ object infra: // these lists are intended to be constructed at compile-time via scala metaprogramming // to reduce errors def named(unames: Map[String, String], pnames: Set[String]): Parser[RuntimeUnit] = - val prefixunit = (strset(pnames) ~ strset(unames.keySet `diff` pnames)) <* Parser.end + val prefixunit: Parser[(String, String)] = + if (pnames.isEmpty || unames.isEmpty) + Parser.fail + else + (strset(pnames) ~ strset(unames.keySet `diff` pnames)) <* Parser.end unitlit.flatMap { name => if (unames.contains(name)) // name is a defined unit, return its type @@ -261,12 +266,10 @@ object meta: import coulomb.infra.meta.{*, given} import coulomb.infra.runtime.meta.{*, given} - import coulomb.conversion.runtimes.mapping.meta.{*, given} + import coulomb.conversion.runtimes.mapping.meta.moduleUnits import coulomb.parser.standard.RuntimeUnitExprParser - import coulomb.syntax.typelist.{TNil, &:} - def ofUTL[UTL](using Quotes, Type[UTL]): Expr[RuntimeUnitExprParser] = import quotes.reflect.* val (un, pn) = collect(typeReprList(TypeRepr.of[UTL])) @@ -275,13 +278,10 @@ object meta: val un1 = un.filter { case (k, _) => k.length > 0 } - if (un1.isEmpty) - // unit map must be non-empty - report.errorAndAbort(s"ofUTL: no defined unit names") '{ new RuntimeUnitExprParser: - protected val unames = ${ Expr(un1) } - protected val pnames = ${ Expr(pn1) } + val unames = ${ Expr(un1) } + val pnames = ${ Expr(pn1) } } private def collect(using Quotes)(tl: List[quotes.reflect.TypeRepr]): (Map[String, String], Set[String]) = @@ -292,7 +292,8 @@ object meta: val (un, pn) = collect(tail) head match case ConstantType(StringConstant(mname)) => - collect(moduleUnits(mname)) + val (mu, mp) = collect(moduleUnits(mname)) + (un ++ mu, pn ++ mp) case baseunitTR(tr) => val AppliedType(_, List(_, n, _)) = tr: @unchecked val ConstantType(StringConstant(name)) = n: @unchecked diff --git a/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala b/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala index ec79573c6..94b105e24 100644 --- a/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala +++ b/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala @@ -58,7 +58,7 @@ extension (lhs: Either[String, Canonical]) lhs.map { l => l.pow(e) } object MappingCoefficientRuntime: - inline def of[UTL]: MappingCoefficientRuntime = ${ meta.ofUTL[UTL] } + inline def of[UTL <: Tuple]: MappingCoefficientRuntime = ${ meta.ofUTL[UTL] } case class Canonical(coef: Rational, sig: Map[RuntimeUnit.UnitType, Rational]): def *(that: Canonical): Canonical = @@ -105,8 +105,6 @@ object meta: import coulomb.infra.meta.{*, given} import coulomb.infra.runtime.meta.{*, given} - import coulomb.syntax.typelist.{TNil, &:} - def ofUTL[UTL](using Quotes, Type[UTL]): Expr[MappingCoefficientRuntime] = import quotes.reflect.* val (bu, du) = utlClosure(typeReprList(TypeRepr.of[UTL])) diff --git a/runtime/src/test/scala/coulomb/mappingquantity.scala b/runtime/src/test/scala/coulomb/mappingquantity.scala index 7a3e92c25..255da5cb2 100644 --- a/runtime/src/test/scala/coulomb/mappingquantity.scala +++ b/runtime/src/test/scala/coulomb/mappingquantity.scala @@ -23,10 +23,8 @@ import coulomb.units.us.{*, given} import coulomb.CoefficientRuntime import coulomb.conversion.runtimes.mapping.MappingCoefficientRuntime -import coulomb.syntax.typelist.{TNil, &:} - val mappingRT: CoefficientRuntime = MappingCoefficientRuntime - .of["coulomb.units.si" &: "coulomb.units.si.prefixes" &: TNil] + .of["coulomb.units.si" *: "coulomb.units.si.prefixes" *: EmptyTuple] class MappingRuntimeQuantitySuite extends RuntimeQuantitySuite(using mappingRT) diff --git a/spire/src/main/scala/coulomb/ops/resolution/spire.scala b/spire/src/main/scala/coulomb/ops/resolution/spire.scala index 166b5e2f1..672d9683e 100644 --- a/spire/src/main/scala/coulomb/ops/resolution/spire.scala +++ b/spire/src/main/scala/coulomb/ops/resolution/spire.scala @@ -20,12 +20,11 @@ object spire: import _root_.spire.math.* import coulomb.ops.ValuePromotionPolicy - import coulomb.syntax.typelist.{&:, TNil} // ValuePromotion infers the transitive closure of all promotions given ctx_vpp_spire: ValuePromotionPolicy[ - (Int, Long) &: (Long, Float) &: (Float, Double) &: - (Double, BigDecimal) &: (BigDecimal, Rational) &: (Long, BigInt) &: - (BigInt, Float) &: (Rational, Algebraic) &: (Algebraic, Real) &: - TNil + (Int, Long) *: (Long, Float) *: (Float, Double) *: + (Double, BigDecimal) *: (BigDecimal, Rational) *: (Long, BigInt) *: + (BigInt, Float) *: (Rational, Algebraic) *: (Algebraic, Real) *: + EmptyTuple ] = ValuePromotionPolicy() From 385f85c6e0d71691afe3ef0e6e34ef85cdbf7cde Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 11 Mar 2023 05:50:54 -0700 Subject: [PATCH 082/141] format --- core/src/main/scala/coulomb/ops/ops.scala | 3 ++- .../scala/coulomb/runtime/conversion/runtimes/mapping.scala | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/core/src/main/scala/coulomb/ops/ops.scala b/core/src/main/scala/coulomb/ops/ops.scala index b63356756..371a22932 100644 --- a/core/src/main/scala/coulomb/ops/ops.scala +++ b/core/src/main/scala/coulomb/ops/ops.scala @@ -255,7 +255,8 @@ object ValuePromotion: final class ValuePromotionPolicy[Pairs <: Tuple] object ValuePromotionPolicy: - def apply[P <: Tuple](): ValuePromotionPolicy[P] = new ValuePromotionPolicy[P] + def apply[P <: Tuple](): ValuePromotionPolicy[P] = + new ValuePromotionPolicy[P] final case class ShowUnit[U](value: String) object ShowUnit: diff --git a/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala b/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala index 94b105e24..921acb60e 100644 --- a/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala +++ b/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala @@ -58,7 +58,9 @@ extension (lhs: Either[String, Canonical]) lhs.map { l => l.pow(e) } object MappingCoefficientRuntime: - inline def of[UTL <: Tuple]: MappingCoefficientRuntime = ${ meta.ofUTL[UTL] } + inline def of[UTL <: Tuple]: MappingCoefficientRuntime = ${ + meta.ofUTL[UTL] + } case class Canonical(coef: Rational, sig: Map[RuntimeUnit.UnitType, Rational]): def *(that: Canonical): Canonical = From d13083ab5b262885b48e19227189d0a232767849 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 11 Mar 2023 07:29:46 -0700 Subject: [PATCH 083/141] factor toRational --- parser/src/main/scala/coulomb/parser.scala | 49 ++++++------------- .../main/scala/coulomb/runtime/runtime.scala | 28 +++++++++++ 2 files changed, 42 insertions(+), 35 deletions(-) diff --git a/parser/src/main/scala/coulomb/parser.scala b/parser/src/main/scala/coulomb/parser.scala index 330b296a5..8ffde5a4c 100644 --- a/parser/src/main/scala/coulomb/parser.scala +++ b/parser/src/main/scala/coulomb/parser.scala @@ -26,13 +26,22 @@ abstract class RuntimeUnitParser: def render(u: RuntimeUnit): String object standard: + import _root_.cats.parse.* + sealed abstract class RuntimeUnitExprParser extends RuntimeUnitParser: def unames: Map[String, String] def pnames: Set[String] + + private lazy val parser: Parser[RuntimeUnit] = + infra.unit(infra.named(unames, pnames)) + def parse(expr: String): Either[String, RuntimeUnit] = - Left("no") + parser.parse(expr) match + case Right((_, u)) => Right(u) + case Left(e) => Left(s"$e") + def render(u: RuntimeUnit): String = - "no" + s"${u.toString}" object RuntimeUnitExprParser: inline def of[UTL <: Tuple]: RuntimeUnitExprParser = @@ -85,9 +94,9 @@ object infra: // used for left-factoring the parsing of "^" (power) val powop: Parser[(RuntimeUnit, RuntimeUnit) => RuntimeUnit] = (Parser.char('^') <* ws0).as { (b: RuntimeUnit, e: RuntimeUnit) => - // we do not have to check for Left value of 'evalnum' here + // we do not have to check for Left value here // because it is verified during parsing - RuntimeUnit.Pow(b, e.evalnum.toSeq.head) + RuntimeUnit.Pow(b, e.toRational.toSeq.head) } def unit(named: Parser[RuntimeUnit]): Parser[RuntimeUnit] = @@ -119,7 +128,7 @@ object infra: // used to enforce that exponent of powers is a valid numeric value lazy val numeric: Parser[RuntimeUnit] = atom.flatMap { u => - u.evalnum match + u.toRational match case Right(v) => Parser.pure(RuntimeUnit.UnitConst(v)) case Left(e) => @@ -182,36 +191,6 @@ object infra: // no common left factor Parser.oneOf(hp) - extension (u: RuntimeUnit) - // attempt to evaluate a RuntimeUnit into a valid - // numeric constant value. This is used to constrain - // specialized parsing of RuntimeUnit expressions that - // are required to represent numeric constants - def evalnum: Either[String, Rational] = - u match - case RuntimeUnit.UnitConst(v) => Right(v) - case RuntimeUnit.Mul(lhs, rhs) => - for { - lv <- lhs.evalnum - rv <- rhs.evalnum - } yield (lv * rv) - case RuntimeUnit.Div(num, den) => - den.evalnum match - case Left(e) => Left(e) - case Right(dv) => - if (dv == Rational.const0) - Left("div by zero") - else - for { - nv <- num.evalnum - } yield (nv / dv) - case RuntimeUnit.Pow(b, e) => - for { - bv <- b.evalnum - } yield bv.pow(e) - case _ => - Left(s"bad numeric expression: $u") - // the following are combinators for factoring left-recursive grammars // they are taken from this paper: // https://github.com/j-mie6/design-patterns-for-parser-combinators#readme diff --git a/runtime/src/main/scala/coulomb/runtime/runtime.scala b/runtime/src/main/scala/coulomb/runtime/runtime.scala index 84bcf6043..8d846f9b9 100644 --- a/runtime/src/main/scala/coulomb/runtime/runtime.scala +++ b/runtime/src/main/scala/coulomb/runtime/runtime.scala @@ -25,6 +25,7 @@ sealed abstract class RuntimeUnit: def *(rhs: RuntimeUnit): RuntimeUnit.Mul = RuntimeUnit.Mul(this, rhs) def /(den: RuntimeUnit): RuntimeUnit.Div = RuntimeUnit.Div(this, den) def ^(e: Rational): RuntimeUnit.Pow = RuntimeUnit.Pow(this, e) + override def toString: String = def paren(s: String, tl: Boolean): String = if (tl) s else s"($s)" @@ -42,6 +43,33 @@ sealed abstract class RuntimeUnit: paren(s"${work(b)}^$e", tl) work(this, tl = true) + // evaluate a RuntimeUnit expression whose leaves are + // all UnitConst into a Rational value + def toRational: Either[String, Rational] = + this match + case RuntimeUnit.UnitConst(v) => Right(v) + case RuntimeUnit.Mul(lhs, rhs) => + for { + lv <- lhs.toRational + rv <- rhs.toRational + } yield (lv * rv) + case RuntimeUnit.Div(num, den) => + den.toRational match + case Left(e) => Left(e) + case Right(dv) => + if (dv == Rational.const0) + Left("toRational: div by zero") + else + for { + nv <- num.toRational + } yield (nv / dv) + case RuntimeUnit.Pow(b, e) => + for { + bv <- b.toRational + } yield bv.pow(e) + case _ => + Left(s"toRational: bad rational expression: $this") + object RuntimeUnit: case class UnitConst(value: Rational) extends RuntimeUnit case class UnitType(path: String) extends RuntimeUnit From 4f2333db8cd3eee59903e90b286b0dc431dd651b Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 11 Mar 2023 08:10:09 -0700 Subject: [PATCH 084/141] render --- parser/src/main/scala/coulomb/parser.scala | 28 +++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/parser/src/main/scala/coulomb/parser.scala b/parser/src/main/scala/coulomb/parser.scala index 8ffde5a4c..7b5f4e4d3 100644 --- a/parser/src/main/scala/coulomb/parser.scala +++ b/parser/src/main/scala/coulomb/parser.scala @@ -35,13 +35,39 @@ object standard: private lazy val parser: Parser[RuntimeUnit] = infra.unit(infra.named(unames, pnames)) + private lazy val unamesinv: Map[String, String] = { + val inv = unames.map { (k, v) => (v, k) } + // check this at compile-time in ofUTL ? + assert(inv.size == unames.size) + inv + } + def parse(expr: String): Either[String, RuntimeUnit] = parser.parse(expr) match case Right((_, u)) => Right(u) case Left(e) => Left(s"$e") def render(u: RuntimeUnit): String = - s"${u.toString}" + def paren(s: String, tl: Boolean): String = + if (tl) s else s"($s)" + def rparen(r: Rational, tl: Boolean): String = + if (r.d == 1) + s"${r.n}" + else + paren(s"${r.n}/${r.d}", tl) + def work(u: RuntimeUnit, tl: Boolean = false): String = + u match + case RuntimeUnit.UnitConst(value) => + rparen(value, tl) + case RuntimeUnit.UnitType(path) => + unamesinv(path) + case RuntimeUnit.Mul(l, r) => + paren(s"${work(l)}*${work(r)}", tl) + case RuntimeUnit.Div(n, d) => + paren(s"${work(n)}/${work(d)}", tl) + case RuntimeUnit.Pow(b, e) => + paren(s"${work(b)}^${rparen(e, false)}", tl) + work(u, tl = true) object RuntimeUnitExprParser: inline def of[UTL <: Tuple]: RuntimeUnitExprParser = From 28e670dd9d14f4a39adc10faff0a82b6663e8805 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 11 Mar 2023 08:44:35 -0700 Subject: [PATCH 085/141] factor parsing --- parser/src/main/scala/coulomb/parser.scala | 217 +---------------- .../scala/coulomb/parser/infra/parser.scala | 221 ++++++++++++++++++ 2 files changed, 232 insertions(+), 206 deletions(-) create mode 100644 parser/src/main/scala/coulomb/parser/infra/parser.scala diff --git a/parser/src/main/scala/coulomb/parser.scala b/parser/src/main/scala/coulomb/parser.scala index 7b5f4e4d3..b8b5965f9 100644 --- a/parser/src/main/scala/coulomb/parser.scala +++ b/parser/src/main/scala/coulomb/parser.scala @@ -23,31 +23,23 @@ import coulomb.rational.Rational abstract class RuntimeUnitParser: def parse(expr: String): Either[String, RuntimeUnit] - def render(u: RuntimeUnit): String + def render(u: RuntimeUnit): Either[String, String] object standard: - import _root_.cats.parse.* - sealed abstract class RuntimeUnitExprParser extends RuntimeUnitParser: def unames: Map[String, String] def pnames: Set[String] - private lazy val parser: Parser[RuntimeUnit] = - infra.unit(infra.named(unames, pnames)) + private lazy val parser: (String => Either[String, RuntimeUnit]) = + infra.parsing.parser(unames, pnames) - private lazy val unamesinv: Map[String, String] = { - val inv = unames.map { (k, v) => (v, k) } - // check this at compile-time in ofUTL ? - assert(inv.size == unames.size) - inv - } + private lazy val unamesinv: Map[String, String] = + unames.map { (k, v) => (v, k) } def parse(expr: String): Either[String, RuntimeUnit] = - parser.parse(expr) match - case Right((_, u)) => Right(u) - case Left(e) => Left(s"$e") + parser(expr) - def render(u: RuntimeUnit): String = + def render(u: RuntimeUnit): Either[String, String] = def paren(s: String, tl: Boolean): String = if (tl) s else s"($s)" def rparen(r: Rational, tl: Boolean): String = @@ -60,6 +52,7 @@ object standard: case RuntimeUnit.UnitConst(value) => rparen(value, tl) case RuntimeUnit.UnitType(path) => + // this can error out if map isn't defined unamesinv(path) case RuntimeUnit.Mul(l, r) => paren(s"${work(l)}*${work(r)}", tl) @@ -67,202 +60,14 @@ object standard: paren(s"${work(n)}/${work(d)}", tl) case RuntimeUnit.Pow(b, e) => paren(s"${work(b)}^${rparen(e, false)}", tl) - work(u, tl = true) + Try { work(u, tl = true) } match + case Success(s) => Right(s) + case Failure(e) => Left(s"$e") object RuntimeUnitExprParser: inline def of[UTL <: Tuple]: RuntimeUnitExprParser = ${ coulomb.parser.meta.ofUTL[UTL] } -object infra: - import _root_.cats.parse.* - - // for consuming whitespace - val ws: Parser[Unit] = Parser.charIn(" \t\r\n").void - val ws0: Parser0[Unit] = ws.rep0.void - - // numeric literals parse into UnitConst objects - val numlit: Parser[RuntimeUnit] = - cats.parse.Numbers.jsonNumber.flatMap { lit => - lit match - case intlit(v) => - Parser.pure(RuntimeUnit.UnitConst(Rational(v, 1))) - case fplit(v) => - Parser.pure(RuntimeUnit.UnitConst(Rational(v))) - case _ => - Parser.failWith[RuntimeUnit](s"bad numeric literal '$lit'") - } - - object intlit: - def unapply(lit: String): Option[BigInt] = - Try { BigInt(lit) }.toOption - - object fplit: - def unapply(lit: String): Option[Double] = - Try { lit.toDouble }.toOption - - // a token representing a unit name literal - // examples: "meter", "second", etc - // note that for any defined prefix and unit, is also valid - // for example if "kilo" and "meter" are defined units, "kilometer" will also - // parse correctly as RuntimeUnit.Mul(Kilo, Meter) - val unitlit: Parser[String] = - // this might be extended but not until I have a reason and a principle - // one possible extension would be "any printable char not in { '(', ')', '*', etc }" - // however I'm not sure if there is an efficient way to express that - // (starting char can also not be digit, + or -) - Parser.charIn('a' to 'z').rep.string - - // used for left-factoring the parsing for sequences of mul and div - val muldivop: Parser[(RuntimeUnit, RuntimeUnit) => RuntimeUnit] = - (Parser.char('*') <* ws0).as(RuntimeUnit.Mul(_, _)) | - (Parser.char('/') <* ws0).as(RuntimeUnit.Div(_, _)) - - // used for left-factoring the parsing of "^" (power) - val powop: Parser[(RuntimeUnit, RuntimeUnit) => RuntimeUnit] = - (Parser.char('^') <* ws0).as { (b: RuntimeUnit, e: RuntimeUnit) => - // we do not have to check for Left value here - // because it is verified during parsing - RuntimeUnit.Pow(b, e.toRational.toSeq.head) - } - - def unit(named: Parser[RuntimeUnit]): Parser[RuntimeUnit] = - lazy val unitexpr: Parser[RuntimeUnit] = Parser.defer { - // sequence of mul and div operators - // these have lowest precedence and form the top of the parse tree - // example: * / * ... - lazy val muldiv: Parser[RuntimeUnit] = - chainl1(pow, muldivop) - - // powers of form ^ , where: - // may be any unit expression and - // is an expression that must evaluate to a valid numeric constant - lazy val pow: Parser[RuntimeUnit] = - binaryl1(atom, numeric, powop) - - // numeric literal, named unit, or sub-expr in parens - lazy val atom: Parser[RuntimeUnit] = - paren | (numlit <* ws0) | (named <* ws0) - - // any unit subexpression inside of parens: () - lazy val paren: Parser[RuntimeUnit] = - unitexpr.between(Parser.char('(') <* ws0, Parser.char(')') <* ws0) - - // parses a RuntimeUnit expression, but only succeeds - // if it evaluates to a constant numeric value - // note this is based on 'atom' so non-atomic expressions may only - // appear inside of () - // used to enforce that exponent of powers is a valid numeric value - lazy val numeric: Parser[RuntimeUnit] = - atom.flatMap { u => - u.toRational match - case Right(v) => - Parser.pure(RuntimeUnit.UnitConst(v)) - case Left(e) => - Parser.failWith[RuntimeUnit](e) - } - - // return the top of the parse tree - muldiv - } - // parse a unit expression, consuming any leading whitespace - // and requiring parsing reach end of input - // (trailing whitespace is consumed inside unitexpr) - ws0.with1 *> unitexpr <* Parser.end - - // parses "raw" unit literals - only succeeds if the literal is - // in the list of defined units (or unit prefixes) - // these lists are intended to be constructed at compile-time via scala metaprogramming - // to reduce errors - def named(unames: Map[String, String], pnames: Set[String]): Parser[RuntimeUnit] = - val prefixunit: Parser[(String, String)] = - if (pnames.isEmpty || unames.isEmpty) - Parser.fail - else - (strset(pnames) ~ strset(unames.keySet `diff` pnames)) <* Parser.end - unitlit.flatMap { name => - if (unames.contains(name)) - // name is a defined unit, return its type - Parser.pure(RuntimeUnit.UnitType(unames(name))) - else - // otherwise see if it can be parsed as - prefixunit.parse(name) match - case Right((_, (pn, un))) => - // => * - val p = RuntimeUnit.UnitType(unames(pn)) - val u = RuntimeUnit.UnitType(unames(un)) - Parser.pure(RuntimeUnit.Mul(p, u)) - case Left(_) => - Parser.failWith[RuntimeUnit](s"unrecognized unit '$name'") - } - - def strset(ss: Set[String]): Parser[String] = - strsetvoid(ss).string - - // assumes ss is not empty and all members are length > 0 - // this is guaranteed by construction at compile time - private def strsetvoid(ss: Set[String]): Parser[Unit] = - // construct a parser "branch" for each starting character - val hp = ss.map(_.head).toList.map { h => - // set of string tails starting with char h - val tails = ss.filter(_.head == h).map(_.drop(1)).filter(_.length > 0) - if (tails.isEmpty) - // no remaining string tails, just parse char h - Parser.char(h) - else - // parse h followed by parser for tails - (Parser.char(h) ~ strsetvoid(tails)).void - } - // final parser is just "or" of branches: hp(0) | hp(1) | hp(2) ... - // these are safe to "or" because by construction they share - // no common left factor - Parser.oneOf(hp) - - // the following are combinators for factoring left-recursive grammars - // they are taken from this paper: - // https://github.com/j-mie6/design-patterns-for-parser-combinators#readme - def chainl1[X](p: Parser[X], op: Parser[(X, X) => X]): Parser[X] = - lazy val rest: Parser0[X => X] = Parser.defer0 { - val some: Parser0[X => X] = (op, p, rest).mapN { - // found an , with possibly more - (f, y, next) => ((x: X) => next(f(x, y))) - } - // none consumes no input - val none: Parser0[X => X] = Parser.pure(identity[X]) - // "some" expected to be distinguished by leading char of "op" - // for example lhs + rhs distinguished by '+' - some | none - } - // this feels wrong but .with1 returns With1, not Parser - rapp(p, rest).asInstanceOf[Parser[X]] - - // like chainl1 but specifically a single left-factored binary expr - // [ ] - def binaryl1[X](pl: Parser[X], pr: Parser[X], op: Parser[(X, X) => X]): Parser[X] = - val some: Parser0[X => X] = (op, pr).mapN { - // found an - (f, y) => ((x: X) => f(x, y)) - } - val none: Parser0[X => X] = Parser.pure(identity[X]) - rapp(pl, some | none).asInstanceOf[Parser[X]] - - // parsley <*> - def app[X, Z](f: Parser0[X => Z], x: Parser0[X]): Parser0[Z] = - (f ~ x).map { case (f, x) => f(x) } - - // parsley <**> - def rapp[X, Z](x: Parser0[X], f: Parser0[X => Z]): Parser0[Z] = - (x ~ f).map { case (x, f) => f(x) } - - // parsley zipped - // can scala 3 '*:' clean this up? - extension [X1, X2](p: (Parser0[X1], Parser0[X2])) - def mapN[Z](f: (X1, X2) => Z): Parser0[Z] = - (p._1 ~ p._2).map { case (x1, x2) => f(x1, x2) } - - extension [X1, X2, X3](p: (Parser0[X1], Parser0[X2], Parser0[X3])) - def mapN[Z](f: (X1, X2, X3) => Z): Parser0[Z] = - ((p._1 ~ p._2) ~ p._3).map { case ((x1, x2), x3) => f(x1, x2, x3) } - object meta: import scala.quoted.* import scala.util.{Try, Success, Failure} diff --git a/parser/src/main/scala/coulomb/parser/infra/parser.scala b/parser/src/main/scala/coulomb/parser/infra/parser.scala new file mode 100644 index 000000000..1aa4fcf2b --- /dev/null +++ b/parser/src/main/scala/coulomb/parser/infra/parser.scala @@ -0,0 +1,221 @@ +/* + * Copyright 2022 Erik Erlandson + * + * 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 coulomb.parser.infra + +import scala.util.{Try, Success, Failure} + +import coulomb.RuntimeUnit +import coulomb.rational.Rational + +object parsing: + def parser(unames: Map[String, String], pnames: Set[String]): (String => Either[String, RuntimeUnit]) = + val p = catsparse.unit(catsparse.named(unames, pnames)) + (expr: String) => p.parse(expr) match + case Right((_, u)) => Right(u) + case Left(e) => Left(s"$e") + + // parsing library is implementation detail + private object catsparse: + import _root_.cats.parse.* + + // for consuming whitespace + val ws: Parser[Unit] = Parser.charIn(" \t\r\n").void + val ws0: Parser0[Unit] = ws.rep0.void + + // numeric literals parse into UnitConst objects + val numlit: Parser[RuntimeUnit] = + cats.parse.Numbers.jsonNumber.flatMap { lit => + lit match + case intlit(v) => + Parser.pure(RuntimeUnit.UnitConst(Rational(v, 1))) + case fplit(v) => + Parser.pure(RuntimeUnit.UnitConst(Rational(v))) + case _ => + Parser.failWith[RuntimeUnit](s"bad numeric literal '$lit'") + } + + object intlit: + def unapply(lit: String): Option[BigInt] = + Try { BigInt(lit) }.toOption + + object fplit: + def unapply(lit: String): Option[Double] = + Try { lit.toDouble }.toOption + + // a token representing a unit name literal + // examples: "meter", "second", etc + // note that for any defined prefix and unit, is also valid + // for example if "kilo" and "meter" are defined units, "kilometer" will also + // parse correctly as RuntimeUnit.Mul(Kilo, Meter) + val unitlit: Parser[String] = + // this might be extended but not until I have a reason and a principle + // one possible extension would be "any printable char not in { '(', ')', '*', etc }" + // however I'm not sure if there is an efficient way to express that + // (starting char can also not be digit, + or -) + Parser.charIn('a' to 'z').rep.string + + // used for left-factoring the parsing for sequences of mul and div + val muldivop: Parser[(RuntimeUnit, RuntimeUnit) => RuntimeUnit] = + (Parser.char('*') <* ws0).as(RuntimeUnit.Mul(_, _)) | + (Parser.char('/') <* ws0).as(RuntimeUnit.Div(_, _)) + + // used for left-factoring the parsing of "^" (power) + val powop: Parser[(RuntimeUnit, RuntimeUnit) => RuntimeUnit] = + (Parser.char('^') <* ws0).as { (b: RuntimeUnit, e: RuntimeUnit) => + // we do not have to check for Left value here + // because it is verified during parsing + RuntimeUnit.Pow(b, e.toRational.toSeq.head) + } + + def unit(named: Parser[RuntimeUnit]): Parser[RuntimeUnit] = + lazy val unitexpr: Parser[RuntimeUnit] = Parser.defer { + // sequence of mul and div operators + // these have lowest precedence and form the top of the parse tree + // example: * / * ... + lazy val muldiv: Parser[RuntimeUnit] = + chainl1(pow, muldivop) + + // powers of form ^ , where: + // may be any unit expression and + // is an expression that must evaluate to a valid numeric constant + lazy val pow: Parser[RuntimeUnit] = + binaryl1(atom, numeric, powop) + + // numeric literal, named unit, or sub-expr in parens + lazy val atom: Parser[RuntimeUnit] = + paren | (numlit <* ws0) | (named <* ws0) + + // any unit subexpression inside of parens: () + lazy val paren: Parser[RuntimeUnit] = + unitexpr.between(Parser.char('(') <* ws0, Parser.char(')') <* ws0) + + // parses a RuntimeUnit expression, but only succeeds + // if it evaluates to a constant numeric value + // note this is based on 'atom' so non-atomic expressions may only + // appear inside of () + // used to enforce that exponent of powers is a valid numeric value + lazy val numeric: Parser[RuntimeUnit] = + atom.flatMap { u => + u.toRational match + case Right(v) => + Parser.pure(RuntimeUnit.UnitConst(v)) + case Left(e) => + Parser.failWith[RuntimeUnit](e) + } + + // return the top of the parse tree + muldiv + } + // parse a unit expression, consuming any leading whitespace + // and requiring parsing reach end of input + // (trailing whitespace is consumed inside unitexpr) + ws0.with1 *> unitexpr <* Parser.end + + // parses "raw" unit literals - only succeeds if the literal is + // in the list of defined units (or unit prefixes) + // these lists are intended to be constructed at compile-time via scala metaprogramming + // to reduce errors + def named(unames: Map[String, String], pnames: Set[String]): Parser[RuntimeUnit] = + val prefixunit: Parser[(String, String)] = + if (pnames.isEmpty || unames.isEmpty) + Parser.fail + else + (strset(pnames) ~ strset(unames.keySet `diff` pnames)) <* Parser.end + unitlit.flatMap { name => + if (unames.contains(name)) + // name is a defined unit, return its type + Parser.pure(RuntimeUnit.UnitType(unames(name))) + else + // otherwise see if it can be parsed as + prefixunit.parse(name) match + case Right((_, (pn, un))) => + // => * + val p = RuntimeUnit.UnitType(unames(pn)) + val u = RuntimeUnit.UnitType(unames(un)) + Parser.pure(RuntimeUnit.Mul(p, u)) + case Left(_) => + Parser.failWith[RuntimeUnit](s"unrecognized unit '$name'") + } + + def strset(ss: Set[String]): Parser[String] = + strsetvoid(ss).string + + // assumes ss is not empty and all members are length > 0 + // this is guaranteed by construction at compile time + private def strsetvoid(ss: Set[String]): Parser[Unit] = + // construct a parser "branch" for each starting character + val hp = ss.map(_.head).toList.map { h => + // set of string tails starting with char h + val tails = ss.filter(_.head == h).map(_.drop(1)).filter(_.length > 0) + if (tails.isEmpty) + // no remaining string tails, just parse char h + Parser.char(h) + else + // parse h followed by parser for tails + (Parser.char(h) ~ strsetvoid(tails)).void + } + // final parser is just "or" of branches: hp(0) | hp(1) | hp(2) ... + // these are safe to "or" because by construction they share + // no common left factor + Parser.oneOf(hp) + + // the following are combinators for factoring left-recursive grammars + // they are taken from this paper: + // https://github.com/j-mie6/design-patterns-for-parser-combinators#readme + def chainl1[X](p: Parser[X], op: Parser[(X, X) => X]): Parser[X] = + lazy val rest: Parser0[X => X] = Parser.defer0 { + val some: Parser0[X => X] = (op, p, rest).mapN { + // found an , with possibly more + (f, y, next) => ((x: X) => next(f(x, y))) + } + // none consumes no input + val none: Parser0[X => X] = Parser.pure(identity[X]) + // "some" expected to be distinguished by leading char of "op" + // for example lhs + rhs distinguished by '+' + some | none + } + // this feels wrong but .with1 returns With1, not Parser + rapp(p, rest).asInstanceOf[Parser[X]] + + // like chainl1 but specifically a single left-factored binary expr + // [ ] + def binaryl1[X](pl: Parser[X], pr: Parser[X], op: Parser[(X, X) => X]): Parser[X] = + val some: Parser0[X => X] = (op, pr).mapN { + // found an + (f, y) => ((x: X) => f(x, y)) + } + val none: Parser0[X => X] = Parser.pure(identity[X]) + rapp(pl, some | none).asInstanceOf[Parser[X]] + + // parsley <*> + def app[X, Z](f: Parser0[X => Z], x: Parser0[X]): Parser0[Z] = + (f ~ x).map { case (f, x) => f(x) } + + // parsley <**> + def rapp[X, Z](x: Parser0[X], f: Parser0[X => Z]): Parser0[Z] = + (x ~ f).map { case (x, f) => f(x) } + + // parsley zipped + // can scala 3 '*:' clean this up? + extension [X1, X2](p: (Parser0[X1], Parser0[X2])) + def mapN[Z](f: (X1, X2) => Z): Parser0[Z] = + (p._1 ~ p._2).map { case (x1, x2) => f(x1, x2) } + + extension [X1, X2, X3](p: (Parser0[X1], Parser0[X2], Parser0[X3])) + def mapN[Z](f: (X1, X2, X3) => Z): Parser0[Z] = + ((p._1 ~ p._2) ~ p._3).map { case ((x1, x2), x3) => f(x1, x2, x3) } + From 99c1c36a911d68d1f3e26d54cb7beab7550378e6 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 11 Mar 2023 08:50:32 -0700 Subject: [PATCH 086/141] factor meta --- parser/src/main/scala/coulomb/parser.scala | 58 +------------- .../scala/coulomb/parser/infra/meta.scala | 76 +++++++++++++++++++ 2 files changed, 78 insertions(+), 56 deletions(-) create mode 100644 parser/src/main/scala/coulomb/parser/infra/meta.scala diff --git a/parser/src/main/scala/coulomb/parser.scala b/parser/src/main/scala/coulomb/parser.scala index b8b5965f9..9a4ec8adb 100644 --- a/parser/src/main/scala/coulomb/parser.scala +++ b/parser/src/main/scala/coulomb/parser.scala @@ -26,7 +26,7 @@ abstract class RuntimeUnitParser: def render(u: RuntimeUnit): Either[String, String] object standard: - sealed abstract class RuntimeUnitExprParser extends RuntimeUnitParser: + abstract class RuntimeUnitExprParser extends RuntimeUnitParser: def unames: Map[String, String] def pnames: Set[String] @@ -66,58 +66,4 @@ object standard: object RuntimeUnitExprParser: inline def of[UTL <: Tuple]: RuntimeUnitExprParser = - ${ coulomb.parser.meta.ofUTL[UTL] } - -object meta: - import scala.quoted.* - import scala.util.{Try, Success, Failure} - import scala.unchecked - import scala.language.implicitConversions - - import coulomb.infra.meta.{*, given} - import coulomb.infra.runtime.meta.{*, given} - import coulomb.conversion.runtimes.mapping.meta.moduleUnits - - import coulomb.parser.standard.RuntimeUnitExprParser - - def ofUTL[UTL](using Quotes, Type[UTL]): Expr[RuntimeUnitExprParser] = - import quotes.reflect.* - val (un, pn) = collect(typeReprList(TypeRepr.of[UTL])) - // remove any unit names that are empty strings - val pn1 = pn.filter(_.length > 0) - val un1 = un.filter { case (k, _) => - k.length > 0 - } - '{ - new RuntimeUnitExprParser: - val unames = ${ Expr(un1) } - val pnames = ${ Expr(pn1) } - } - - private def collect(using Quotes)(tl: List[quotes.reflect.TypeRepr]): (Map[String, String], Set[String]) = - import quotes.reflect.* - tl match - case Nil => (Map.empty[String, String], Set.empty[String]) - case head :: tail => - val (un, pn) = collect(tail) - head match - case ConstantType(StringConstant(mname)) => - val (mu, mp) = collect(moduleUnits(mname)) - (un ++ mu, pn ++ mp) - case baseunitTR(tr) => - val AppliedType(_, List(_, n, _)) = tr: @unchecked - val ConstantType(StringConstant(name)) = n: @unchecked - // base units are never prefix units because prefix units are - // derived from '1' (unitless) - (un + (name -> head.typeSymbol.fullName), pn) - case derivedunitTR(tr) => - val AppliedType(_, List(_, _, n, _)) = tr: @unchecked - val ConstantType(StringConstant(name)) = n: @unchecked - // always add to unit types - val unr = un + (name -> head.typeSymbol.fullName) - // if it is derived from unitless, also add it to prefix unit set - val pnr = if (convertible(head, TypeRepr.of[1])) pn + name else pn - (unr, pnr) - case _ => - report.errorAndAbort(s"collect: bad type ${head.show}") - null.asInstanceOf[Nothing] + ${ infra.meta.ofUTL[UTL] } diff --git a/parser/src/main/scala/coulomb/parser/infra/meta.scala b/parser/src/main/scala/coulomb/parser/infra/meta.scala new file mode 100644 index 000000000..67af25929 --- /dev/null +++ b/parser/src/main/scala/coulomb/parser/infra/meta.scala @@ -0,0 +1,76 @@ +/* + * Copyright 2022 Erik Erlandson + * + * 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 coulomb.parser.infra + +import scala.util.{Try, Success, Failure} + +import coulomb.RuntimeUnit +import coulomb.rational.Rational + +object meta: + import scala.quoted.* + import scala.util.{Try, Success, Failure} + import scala.unchecked + import scala.language.implicitConversions + + import coulomb.infra.meta.{*, given} + import coulomb.infra.runtime.meta.{*, given} + import coulomb.conversion.runtimes.mapping.meta.moduleUnits + + import coulomb.parser.standard.RuntimeUnitExprParser + + def ofUTL[UTL](using Quotes, Type[UTL]): Expr[RuntimeUnitExprParser] = + import quotes.reflect.* + val (un, pn) = collect(typeReprList(TypeRepr.of[UTL])) + // remove any unit names that are empty strings + val pn1 = pn.filter(_.length > 0) + val un1 = un.filter { case (k, _) => + k.length > 0 + } + '{ + new RuntimeUnitExprParser: + val unames = ${ Expr(un1) } + val pnames = ${ Expr(pn1) } + } + + private def collect(using Quotes)(tl: List[quotes.reflect.TypeRepr]): (Map[String, String], Set[String]) = + import quotes.reflect.* + tl match + case Nil => (Map.empty[String, String], Set.empty[String]) + case head :: tail => + val (un, pn) = collect(tail) + head match + case ConstantType(StringConstant(mname)) => + val (mu, mp) = collect(moduleUnits(mname)) + (un ++ mu, pn ++ mp) + case baseunitTR(tr) => + val AppliedType(_, List(_, n, _)) = tr: @unchecked + val ConstantType(StringConstant(name)) = n: @unchecked + // base units are never prefix units because prefix units are + // derived from '1' (unitless) + (un + (name -> head.typeSymbol.fullName), pn) + case derivedunitTR(tr) => + val AppliedType(_, List(_, _, n, _)) = tr: @unchecked + val ConstantType(StringConstant(name)) = n: @unchecked + // always add to unit types + val unr = un + (name -> head.typeSymbol.fullName) + // if it is derived from unitless, also add it to prefix unit set + val pnr = if (convertible(head, TypeRepr.of[1])) pn + name else pn + (unr, pnr) + case _ => + report.errorAndAbort(s"collect: bad type ${head.show}") + null.asInstanceOf[Nothing] From 8526e42444b6ffb5e75a333e2bef0273c90ea870 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 11 Mar 2023 09:48:08 -0700 Subject: [PATCH 087/141] parsing.scala --- .../scala/coulomb/parser/infra/{parser.scala => parsing.scala} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename parser/src/main/scala/coulomb/parser/infra/{parser.scala => parsing.scala} (100%) diff --git a/parser/src/main/scala/coulomb/parser/infra/parser.scala b/parser/src/main/scala/coulomb/parser/infra/parsing.scala similarity index 100% rename from parser/src/main/scala/coulomb/parser/infra/parser.scala rename to parser/src/main/scala/coulomb/parser/infra/parsing.scala From 5546c2c210432c85ca4154f8829d6997afbed1a2 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 12 Mar 2023 08:22:30 -0700 Subject: [PATCH 088/141] ConfigReader[Quantity[U, V]] --- .../src/main/scala/coulomb/pureconfig.scala | 69 ++++++++++++++++++- .../main/scala/coulomb/runtime/runtime.scala | 10 +-- 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/pureconfig/src/main/scala/coulomb/pureconfig.scala b/pureconfig/src/main/scala/coulomb/pureconfig.scala index a2bfa4018..77c4c4f46 100644 --- a/pureconfig/src/main/scala/coulomb/pureconfig.scala +++ b/pureconfig/src/main/scala/coulomb/pureconfig.scala @@ -18,11 +18,78 @@ package coulomb import _root_.pureconfig.* +import algebra.ring.MultiplicativeSemigroup + import coulomb.{infra => _, *} +import coulomb.syntax.* import coulomb.rational.Rational +import coulomb.conversion.ValueConversion object pureconfig: - val stub = 0 + import scala.util.{Try, Success, Failure} + + import _root_.pureconfig.error.CannotConvert + + import coulomb.parser.RuntimeUnitParser + + object intlit: + def unapply(lit: String): Option[BigInt] = + Try { BigInt(lit) }.toOption + + object fplit: + def unapply(lit: String): Option[Double] = + Try { lit.toDouble }.toOption + + object ratlit: + def unapply(lit: String): Option[Rational] = + Try { + val x = lit.filter(_ != ' ').split('/') + assert(x.length == 2) + Rational(BigInt(x(0)), BigInt(x(1))) + }.toOption + + given ctx_Rational_Reader: ConfigReader[Rational] = + ConfigReader.fromCursor[Rational] { cur => + cur.asString.flatMap { lit => + lit match + case intlit(v) => Right(Rational(v, 1)) + case fplit(v) => Right(Rational(v)) + case ratlit(v) => Right(v) + case _ => cur.failed(CannotConvert(lit, "Rational", s"bad rational literal")) + } + } + + given ctx_RuntimeUnit_Reader(using + parser: RuntimeUnitParser + ): ConfigReader[RuntimeUnit] = + ConfigReader.fromCursor[RuntimeUnit] { cur => + cur.asString.flatMap { expr => + parser.parse(expr) match + case Right(u) => Right(u) + case Left(e) => cur.failed(CannotConvert(expr, "RuntimeUnit", e)) + } + } + + given ctx_RuntimeQuantity_Reader[V](using + ConfigReader[V], + ConfigReader[RuntimeUnit] + ): ConfigReader[RuntimeQuantity[V]] = + ConfigReader.forProduct2("value", "unit") { (v: V, u: RuntimeUnit) => + RuntimeQuantity(v, u) + } + + inline given ctx_Quantity_Reader[V, U](using + crv: ConfigReader[V], + cru: ConfigReader[RuntimeUnit], + crt: CoefficientRuntime, + vcr: ValueConversion[Rational, V], + mul: MultiplicativeSemigroup[V] + ): ConfigReader[Quantity[V, U]] = + ConfigReader[RuntimeQuantity[V]].emap { rq => + crt.coefficient[V](rq.unit, RuntimeUnit.of[U]) match + case Right(coef) => Right(mul.times(coef, rq.value).withUnit[U]) + case Left(e) => Left(CannotConvert(s"$rq", "Quantity", e)) + } object pureconfig_save: // it would be nice to handle polymorphic inputs diff --git a/runtime/src/main/scala/coulomb/runtime/runtime.scala b/runtime/src/main/scala/coulomb/runtime/runtime.scala index 8d846f9b9..81ca706c6 100644 --- a/runtime/src/main/scala/coulomb/runtime/runtime.scala +++ b/runtime/src/main/scala/coulomb/runtime/runtime.scala @@ -96,6 +96,7 @@ package syntax { case class RuntimeQuantity[V](value: V, unit: RuntimeUnit) object RuntimeQuantity: + import algebra.ring.MultiplicativeSemigroup import coulomb.ops.* inline def apply[V, U](q: Quantity[V, U]): RuntimeQuantity[V] = @@ -112,11 +113,12 @@ object RuntimeQuantity: extension [VL](ql: RuntimeQuantity[VL]) inline def toQuantity[VR, UR](using crt: CoefficientRuntime, - vi: ValueConversion[VL, Rational], - vo: ValueConversion[Rational, VR] + vc: ValueConversion[VL, VR], + vcr: ValueConversion[Rational, VR], + mul: MultiplicativeSemigroup[VR] ): Either[String, Quantity[VR, UR]] = - crt.coefficientRational[UR](ql.unit).map { coef => - vo(coef * vi(ql.value)).withUnit[UR] + crt.coefficient[VR](ql.unit, RuntimeUnit.of[UR]).map { coef => + mul.times(coef, vc(ql.value)).withUnit[UR] } transparent inline def +[VR](qr: RuntimeQuantity[VR])(using From d369bd678af9bfc47d48e1fc9384f3719ac8c2b9 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 12 Mar 2023 11:50:48 -0700 Subject: [PATCH 089/141] remove hierarchy pattern example --- .../src/main/scala/coulomb/pureconfig.scala | 69 ------------------- 1 file changed, 69 deletions(-) diff --git a/pureconfig/src/main/scala/coulomb/pureconfig.scala b/pureconfig/src/main/scala/coulomb/pureconfig.scala index 77c4c4f46..8c62df398 100644 --- a/pureconfig/src/main/scala/coulomb/pureconfig.scala +++ b/pureconfig/src/main/scala/coulomb/pureconfig.scala @@ -90,72 +90,3 @@ object pureconfig: case Right(coef) => Right(mul.times(coef, rq.value).withUnit[U]) case Left(e) => Left(CannotConvert(s"$rq", "Quantity", e)) } - -object pureconfig_save: - // it would be nice to handle polymorphic inputs - // https://github.com/pureconfig/pureconfig/issues/1472 - given rationalReader: ConfigReader[Rational] = - ConfigReader.forProduct2("n", "d") { (n: BigInt, d: BigInt) => - Rational(n, d) - } - - given rationalWriter: ConfigWriter[Rational] = - ConfigWriter.forProduct2("n", "d") { (r: Rational) => - (r.n, r.d) - } - - given runtimeUnitReader: ConfigReader[RuntimeUnit] = - ConfigReader.fromCursor { xcur => - for { - cur <- xcur.asObjectCursor - typecur <- cur.atKey("type") - typestr <- typecur.asString - u <- infra.readByType(typestr, cur) - } yield u - } - - given runtimeUnitWriter: ConfigWriter[RuntimeUnit] = - ConfigWriter.fromFunction { (x: RuntimeUnit) => - x match - case u: RuntimeUnit.UnitConst => infra.constWriter.to(u) - case u: RuntimeUnit.UnitType => infra.typeWriter.to(u) - case u: RuntimeUnit.Mul => infra.mulWriter.to(u) - case u: RuntimeUnit.Div => infra.divWriter.to(u) - case u: RuntimeUnit.Pow => infra.powWriter.to(u) - } - - object infra: - import _root_.pureconfig.error.CannotConvert - - def readByType(typ: String, cur: ConfigObjectCursor): ConfigReader.Result[RuntimeUnit] = - typ match - case "const" => constReader.from(cur) - case "type" => typeReader.from(cur) - case "mul" => mulReader.from(cur) - case "div" => divReader.from(cur) - case "pow" => powReader.from(cur) - case t => - cur.failed(CannotConvert(cur.objValue.toString, "RuntimeUnit", s"unknown type $t")) - - val constReader = ConfigReader.forProduct1("value")(RuntimeUnit.UnitConst(_)) - val typeReader = ConfigReader.forProduct1("path")(RuntimeUnit.UnitType(_)) - val mulReader = ConfigReader.forProduct2("lhs", "rhs")(RuntimeUnit.Mul(_, _)) - val divReader = ConfigReader.forProduct2("num", "den")(RuntimeUnit.Div(_, _)) - val powReader = ConfigReader.forProduct2("b", "e")(RuntimeUnit.Pow(_, _)) - - val constWriter = ConfigWriter.forProduct2("type", "value") { - (u: RuntimeUnit.UnitConst) => ("const", u.value) - } - val typeWriter = ConfigWriter.forProduct2("type", "path") { - (u: RuntimeUnit.UnitType) => ("type", u.path) - } - val mulWriter = ConfigWriter.forProduct3("type", "lhs", "rhs") { - (u: RuntimeUnit.Mul) => ("mul", u.lhs, u.rhs) - } - val divWriter = ConfigWriter.forProduct3("type", "num", "den") { - (u: RuntimeUnit.Div) => ("div", u.num, u.den) - } - val powWriter = ConfigWriter.forProduct3("type", "b", "e") { - (u: RuntimeUnit.Pow) => ("pow", u.b, u.e) - } - From ca31c89eb48c0c83f7bded1bff6474f351c0e524 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 12 Mar 2023 16:35:08 -0700 Subject: [PATCH 090/141] ConfigWriter[Quantity[V, U]] --- parser/src/main/scala/coulomb/parser.scala | 26 +++++++------- .../scala/coulomb/parser/infra/parsing.scala | 30 ++++++++++++---- .../src/main/scala/coulomb/pureconfig.scala | 35 +++++++++++++++++-- 3 files changed, 71 insertions(+), 20 deletions(-) diff --git a/parser/src/main/scala/coulomb/parser.scala b/parser/src/main/scala/coulomb/parser.scala index 9a4ec8adb..238a9bd16 100644 --- a/parser/src/main/scala/coulomb/parser.scala +++ b/parser/src/main/scala/coulomb/parser.scala @@ -23,23 +23,23 @@ import coulomb.rational.Rational abstract class RuntimeUnitParser: def parse(expr: String): Either[String, RuntimeUnit] - def render(u: RuntimeUnit): Either[String, String] + def render(u: RuntimeUnit): String object standard: abstract class RuntimeUnitExprParser extends RuntimeUnitParser: def unames: Map[String, String] def pnames: Set[String] - private lazy val parser: (String => Either[String, RuntimeUnit]) = - infra.parsing.parser(unames, pnames) - - private lazy val unamesinv: Map[String, String] = + lazy val unamesinv: Map[String, String] = unames.map { (k, v) => (v, k) } - def parse(expr: String): Either[String, RuntimeUnit] = + private lazy val parser: (String => Either[String, RuntimeUnit]) = + infra.parsing.parser(unames, pnames, unamesinv) + + final def parse(expr: String): Either[String, RuntimeUnit] = parser(expr) - def render(u: RuntimeUnit): Either[String, String] = + final def render(u: RuntimeUnit): String = def paren(s: String, tl: Boolean): String = if (tl) s else s"($s)" def rparen(r: Rational, tl: Boolean): String = @@ -52,17 +52,19 @@ object standard: case RuntimeUnit.UnitConst(value) => rparen(value, tl) case RuntimeUnit.UnitType(path) => - // this can error out if map isn't defined - unamesinv(path) + if (unamesinv.contains(path)) + // if it is in the inverse mapping write the name + unamesinv(path) + else + // otherwise write the fully qualified type name + s"@$path" case RuntimeUnit.Mul(l, r) => paren(s"${work(l)}*${work(r)}", tl) case RuntimeUnit.Div(n, d) => paren(s"${work(n)}/${work(d)}", tl) case RuntimeUnit.Pow(b, e) => paren(s"${work(b)}^${rparen(e, false)}", tl) - Try { work(u, tl = true) } match - case Success(s) => Right(s) - case Failure(e) => Left(s"$e") + work(u, tl = true) object RuntimeUnitExprParser: inline def of[UTL <: Tuple]: RuntimeUnitExprParser = diff --git a/parser/src/main/scala/coulomb/parser/infra/parsing.scala b/parser/src/main/scala/coulomb/parser/infra/parsing.scala index 1aa4fcf2b..205c7f865 100644 --- a/parser/src/main/scala/coulomb/parser/infra/parsing.scala +++ b/parser/src/main/scala/coulomb/parser/infra/parsing.scala @@ -22,8 +22,8 @@ import coulomb.RuntimeUnit import coulomb.rational.Rational object parsing: - def parser(unames: Map[String, String], pnames: Set[String]): (String => Either[String, RuntimeUnit]) = - val p = catsparse.unit(catsparse.named(unames, pnames)) + def parser(unames: Map[String, String], pnames: Set[String], unamesinv: Map[String, String]): (String => Either[String, RuntimeUnit]) = + val p = catsparse.unit(catsparse.named(unames, pnames), catsparse.typed(unamesinv)) (expr: String) => p.parse(expr) match case Right((_, u)) => Right(u) case Left(e) => Left(s"$e") @@ -33,7 +33,7 @@ object parsing: import _root_.cats.parse.* // for consuming whitespace - val ws: Parser[Unit] = Parser.charIn(" \t\r\n").void + val ws: Parser[Unit] = Parser.charIn(" \t").void val ws0: Parser0[Unit] = ws.rep0.void // numeric literals parse into UnitConst objects @@ -66,7 +66,16 @@ object parsing: // one possible extension would be "any printable char not in { '(', ')', '*', etc }" // however I'm not sure if there is an efficient way to express that // (starting char can also not be digit, + or -) - Parser.charIn('a' to 'z').rep.string + Rfc5234.alpha.rep.string + + // scala identifier + val idlit: Parser[String] = + (Rfc5234.alpha ~ (Rfc5234.alpha | Rfc5234.digit | Parser.char('$')).rep0).string + + // fully qualified scala module path for a UnitType + val typelit: Parser[String] = + // I expect at least one '.' in the type path + Parser.char('@') *> (idlit ~ (Parser.char('.') ~ idlit).rep).string // used for left-factoring the parsing for sequences of mul and div val muldivop: Parser[(RuntimeUnit, RuntimeUnit) => RuntimeUnit] = @@ -81,7 +90,7 @@ object parsing: RuntimeUnit.Pow(b, e.toRational.toSeq.head) } - def unit(named: Parser[RuntimeUnit]): Parser[RuntimeUnit] = + def unit(named: Parser[RuntimeUnit], typed: Parser[RuntimeUnit]): Parser[RuntimeUnit] = lazy val unitexpr: Parser[RuntimeUnit] = Parser.defer { // sequence of mul and div operators // these have lowest precedence and form the top of the parse tree @@ -97,7 +106,7 @@ object parsing: // numeric literal, named unit, or sub-expr in parens lazy val atom: Parser[RuntimeUnit] = - paren | (numlit <* ws0) | (named <* ws0) + paren | (numlit <* ws0) | (typed <* ws0) | (named <* ws0) // any unit subexpression inside of parens: () lazy val paren: Parser[RuntimeUnit] = @@ -125,6 +134,15 @@ object parsing: // (trailing whitespace is consumed inside unitexpr) ws0.with1 *> unitexpr <* Parser.end + def typed(unamesinv: Map[String, String]): Parser[RuntimeUnit] = + typelit.flatMap { path => + if (unamesinv.contains(path)) + // type paths are ok if they are in the map + Parser.pure(RuntimeUnit.UnitType(path)) + else + Parser.failWith[RuntimeUnit](s"unrecognized unit type '$path'") + } + // parses "raw" unit literals - only succeeds if the literal is // in the list of defined units (or unit prefixes) // these lists are intended to be constructed at compile-time via scala metaprogramming diff --git a/pureconfig/src/main/scala/coulomb/pureconfig.scala b/pureconfig/src/main/scala/coulomb/pureconfig.scala index 8c62df398..b5e9e7bd7 100644 --- a/pureconfig/src/main/scala/coulomb/pureconfig.scala +++ b/pureconfig/src/main/scala/coulomb/pureconfig.scala @@ -31,6 +31,16 @@ object pureconfig: import _root_.pureconfig.error.CannotConvert import coulomb.parser.RuntimeUnitParser + + import com.typesafe.config.ConfigValue + + // probably useful for unit testing, will keep them here for now + extension [V, U](q: Quantity[V, U]) + inline def toCV(using ConfigWriter[Quantity[V, U]]): ConfigValue = + ConfigWriter[Quantity[V, U]].to(q) + extension (conf: ConfigValue) + inline def toQuantity[V, U](using ConfigReader[Quantity[V, U]]): Quantity[V, U] = + ConfigReader[Quantity[V, U]].from(conf).toSeq.head object intlit: def unapply(lit: String): Option[BigInt] = @@ -70,6 +80,13 @@ object pureconfig: } } + given ctx_RuntimeUnit_Writer(using + parser: RuntimeUnitParser + ): ConfigWriter[RuntimeUnit] = + ConfigWriter[String].contramap[RuntimeUnit] { u => + parser.render(u) + } + given ctx_RuntimeQuantity_Reader[V](using ConfigReader[V], ConfigReader[RuntimeUnit] @@ -78,9 +95,16 @@ object pureconfig: RuntimeQuantity(v, u) } + given ctx_RuntimeQuantity_Writer[V](using + ConfigWriter[V], + ConfigWriter[RuntimeUnit] + ): ConfigWriter[RuntimeQuantity[V]] = + ConfigWriter.forProduct2("value", "unit") { (q: RuntimeQuantity[V]) => + (q.value, q.unit) + } + inline given ctx_Quantity_Reader[V, U](using - crv: ConfigReader[V], - cru: ConfigReader[RuntimeUnit], + crq: ConfigReader[RuntimeQuantity[V]], crt: CoefficientRuntime, vcr: ValueConversion[Rational, V], mul: MultiplicativeSemigroup[V] @@ -90,3 +114,10 @@ object pureconfig: case Right(coef) => Right(mul.times(coef, rq.value).withUnit[U]) case Left(e) => Left(CannotConvert(s"$rq", "Quantity", e)) } + + inline given ctx_Quantity_Writer[V, U](using + ConfigWriter[RuntimeQuantity[V]] + ): ConfigWriter[Quantity[V, U]] = + ConfigWriter[RuntimeQuantity[V]].contramap[Quantity[V, U]] { q => + RuntimeQuantity(q.value, RuntimeUnit.of[U]) + } From c20304f0749d41d236c64f4d9b27292b335f411b Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Tue, 14 Mar 2023 14:38:07 -0700 Subject: [PATCH 091/141] Rational polymorphic i/o --- .../src/main/scala/coulomb/pureconfig.scala | 54 ++++++++++--------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/pureconfig/src/main/scala/coulomb/pureconfig.scala b/pureconfig/src/main/scala/coulomb/pureconfig.scala index b5e9e7bd7..b44aa7e51 100644 --- a/pureconfig/src/main/scala/coulomb/pureconfig.scala +++ b/pureconfig/src/main/scala/coulomb/pureconfig.scala @@ -16,6 +16,7 @@ package coulomb +import scala.jdk.CollectionConverters.* import _root_.pureconfig.* import algebra.ring.MultiplicativeSemigroup @@ -32,7 +33,7 @@ object pureconfig: import coulomb.parser.RuntimeUnitParser - import com.typesafe.config.ConfigValue + import com.typesafe.config.{ConfigValue, ConfigValueFactory} // probably useful for unit testing, will keep them here for now extension [V, U](q: Quantity[V, U]) @@ -42,31 +43,32 @@ object pureconfig: inline def toQuantity[V, U](using ConfigReader[Quantity[V, U]]): Quantity[V, U] = ConfigReader[Quantity[V, U]].from(conf).toSeq.head - object intlit: - def unapply(lit: String): Option[BigInt] = - Try { BigInt(lit) }.toOption - - object fplit: - def unapply(lit: String): Option[Double] = - Try { lit.toDouble }.toOption - - object ratlit: - def unapply(lit: String): Option[Rational] = - Try { - val x = lit.filter(_ != ' ').split('/') - assert(x.length == 2) - Rational(BigInt(x(0)), BigInt(x(1))) - }.toOption - - given ctx_Rational_Reader: ConfigReader[Rational] = - ConfigReader.fromCursor[Rational] { cur => - cur.asString.flatMap { lit => - lit match - case intlit(v) => Right(Rational(v, 1)) - case fplit(v) => Right(Rational(v)) - case ratlit(v) => Right(v) - case _ => cur.failed(CannotConvert(lit, "Rational", s"bad rational literal")) - } + given ctx_RationalReader: ConfigReader[Rational] = + ConfigReader[BigInt].map(Rational(_, 1)) `orElse` + ConfigReader[Double].map(Rational(_)) `orElse` + ConfigReader.forProduct2("n", "d") { (n: BigInt, d: BigInt) => + Rational(n, d) + } + + extension (v: BigInt) + def toCV(using + ConfigWriter[Int], + ConfigWriter[BigInt] + ): ConfigValue = + if (v.isValidInt) ConfigWriter[Int].to(v.toInt) + else ConfigWriter[BigInt].to(v) + + given ctx_RationalWriter(using + ConfigWriter[Int], + ConfigWriter[BigInt] + ): ConfigWriter[Rational] = + ConfigWriter.fromFunction[Rational] { r => + if (r.d == 1) + ConfigValueFactory.fromAnyRef(r.n.toCV) + else + ConfigValueFactory.fromAnyRef( + Map("n" -> r.n.toCV, "d" -> r.d.toCV).asJava + ) } given ctx_RuntimeUnit_Reader(using From c3805b4b3871fded6e39d3ec46b4ec85bca70622 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 6 Aug 2023 10:36:17 -0700 Subject: [PATCH 092/141] factor pureconfig readers and writers into policies --- .../src/main/scala/coulomb/pureconfig.scala | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/pureconfig/src/main/scala/coulomb/pureconfig.scala b/pureconfig/src/main/scala/coulomb/pureconfig.scala index b44aa7e51..dfcb47cf3 100644 --- a/pureconfig/src/main/scala/coulomb/pureconfig.scala +++ b/pureconfig/src/main/scala/coulomb/pureconfig.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package coulomb +package coulomb.pureconfig import scala.jdk.CollectionConverters.* import _root_.pureconfig.* @@ -26,15 +26,21 @@ import coulomb.syntax.* import coulomb.rational.Rational import coulomb.conversion.ValueConversion -object pureconfig: - import scala.util.{Try, Success, Failure} +import scala.util.{Try, Success, Failure} - import _root_.pureconfig.error.CannotConvert +import _root_.pureconfig.error.CannotConvert - import coulomb.parser.RuntimeUnitParser - - import com.typesafe.config.{ConfigValue, ConfigValueFactory} +import coulomb.parser.RuntimeUnitParser +import com.typesafe.config.{ConfigValue, ConfigValueFactory} + +object policy: + object DSL: + export _root_.coulomb.pureconfig.rational.given + export _root_.coulomb.pureconfig.ruDSL.given + export _root_.coulomb.pureconfig.quantity.given + +object testing: // probably useful for unit testing, will keep them here for now extension [V, U](q: Quantity[V, U]) inline def toCV(using ConfigWriter[Quantity[V, U]]): ConfigValue = @@ -43,6 +49,7 @@ object pureconfig: inline def toQuantity[V, U](using ConfigReader[Quantity[V, U]]): Quantity[V, U] = ConfigReader[Quantity[V, U]].from(conf).toSeq.head +object rational: given ctx_RationalReader: ConfigReader[Rational] = ConfigReader[BigInt].map(Rational(_, 1)) `orElse` ConfigReader[Double].map(Rational(_)) `orElse` @@ -71,6 +78,7 @@ object pureconfig: ) } +object ruDSL: given ctx_RuntimeUnit_Reader(using parser: RuntimeUnitParser ): ConfigReader[RuntimeUnit] = @@ -89,6 +97,7 @@ object pureconfig: parser.render(u) } +object quantity: given ctx_RuntimeQuantity_Reader[V](using ConfigReader[V], ConfigReader[RuntimeUnit] From 3fac01cd08b37e202c6bc68b9a74aa1ef2ce7df6 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 6 Aug 2023 12:08:15 -0700 Subject: [PATCH 093/141] factor out dsl to dsl.scala --- .../scala/coulomb/parser/{infra/parsing.scala => dsl.scala} | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) rename parser/src/main/scala/coulomb/parser/{infra/parsing.scala => dsl.scala} (98%) diff --git a/parser/src/main/scala/coulomb/parser/infra/parsing.scala b/parser/src/main/scala/coulomb/parser/dsl.scala similarity index 98% rename from parser/src/main/scala/coulomb/parser/infra/parsing.scala rename to parser/src/main/scala/coulomb/parser/dsl.scala index 205c7f865..7d32897a9 100644 --- a/parser/src/main/scala/coulomb/parser/infra/parsing.scala +++ b/parser/src/main/scala/coulomb/parser/dsl.scala @@ -14,14 +14,14 @@ * limitations under the License. */ -package coulomb.parser.infra +package coulomb.parser import scala.util.{Try, Success, Failure} import coulomb.RuntimeUnit import coulomb.rational.Rational -object parsing: +object dsl: def parser(unames: Map[String, String], pnames: Set[String], unamesinv: Map[String, String]): (String => Either[String, RuntimeUnit]) = val p = catsparse.unit(catsparse.named(unames, pnames), catsparse.typed(unamesinv)) (expr: String) => p.parse(expr) match @@ -29,6 +29,7 @@ object parsing: case Left(e) => Left(s"$e") // parsing library is implementation detail + // note this is private and so could be swapped out without breaking binary compat private object catsparse: import _root_.cats.parse.* From 0aa98cc407640c058ddc6a6ecaaee557401dc991 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 6 Aug 2023 12:14:04 -0700 Subject: [PATCH 094/141] fix new dsl path --- parser/src/main/scala/coulomb/parser.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parser/src/main/scala/coulomb/parser.scala b/parser/src/main/scala/coulomb/parser.scala index 238a9bd16..58c1e3392 100644 --- a/parser/src/main/scala/coulomb/parser.scala +++ b/parser/src/main/scala/coulomb/parser.scala @@ -34,7 +34,7 @@ object standard: unames.map { (k, v) => (v, k) } private lazy val parser: (String => Either[String, RuntimeUnit]) = - infra.parsing.parser(unames, pnames, unamesinv) + dsl.parser(unames, pnames, unamesinv) final def parse(expr: String): Either[String, RuntimeUnit] = parser(expr) From e9d38455e626859a2b045019e674fe5fdece887e Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 6 Aug 2023 12:32:18 -0700 Subject: [PATCH 095/141] rename to RuntimeUnitDslParser --- parser/src/main/scala/coulomb/parser.scala | 6 +++--- parser/src/main/scala/coulomb/parser/infra/meta.scala | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/parser/src/main/scala/coulomb/parser.scala b/parser/src/main/scala/coulomb/parser.scala index 58c1e3392..d9510faaa 100644 --- a/parser/src/main/scala/coulomb/parser.scala +++ b/parser/src/main/scala/coulomb/parser.scala @@ -26,7 +26,7 @@ abstract class RuntimeUnitParser: def render(u: RuntimeUnit): String object standard: - abstract class RuntimeUnitExprParser extends RuntimeUnitParser: + abstract class RuntimeUnitDslParser extends RuntimeUnitParser: def unames: Map[String, String] def pnames: Set[String] @@ -66,6 +66,6 @@ object standard: paren(s"${work(b)}^${rparen(e, false)}", tl) work(u, tl = true) - object RuntimeUnitExprParser: - inline def of[UTL <: Tuple]: RuntimeUnitExprParser = + object RuntimeUnitDslParser: + inline def of[UTL <: Tuple]: RuntimeUnitDslParser = ${ infra.meta.ofUTL[UTL] } diff --git a/parser/src/main/scala/coulomb/parser/infra/meta.scala b/parser/src/main/scala/coulomb/parser/infra/meta.scala index 67af25929..6e65206a1 100644 --- a/parser/src/main/scala/coulomb/parser/infra/meta.scala +++ b/parser/src/main/scala/coulomb/parser/infra/meta.scala @@ -31,9 +31,9 @@ object meta: import coulomb.infra.runtime.meta.{*, given} import coulomb.conversion.runtimes.mapping.meta.moduleUnits - import coulomb.parser.standard.RuntimeUnitExprParser + import coulomb.parser.standard.RuntimeUnitDslParser - def ofUTL[UTL](using Quotes, Type[UTL]): Expr[RuntimeUnitExprParser] = + def ofUTL[UTL](using Quotes, Type[UTL]): Expr[RuntimeUnitDslParser] = import quotes.reflect.* val (un, pn) = collect(typeReprList(TypeRepr.of[UTL])) // remove any unit names that are empty strings @@ -42,7 +42,7 @@ object meta: k.length > 0 } '{ - new RuntimeUnitExprParser: + new RuntimeUnitDslParser: val unames = ${ Expr(un1) } val pnames = ${ Expr(pn1) } } From fa73afccabeb90aaf495e03111b52bd684fbc36c Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 6 Aug 2023 14:42:57 -0700 Subject: [PATCH 096/141] PureconfigRuntime --- parser/src/main/scala/coulomb/parser.scala | 2 +- .../src/main/scala/coulomb/pureconfig.scala | 24 +++++++++++++++++++ .../main/scala/coulomb/runtime/runtime.scala | 2 +- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/parser/src/main/scala/coulomb/parser.scala b/parser/src/main/scala/coulomb/parser.scala index d9510faaa..b665b14c1 100644 --- a/parser/src/main/scala/coulomb/parser.scala +++ b/parser/src/main/scala/coulomb/parser.scala @@ -21,7 +21,7 @@ import scala.util.{Try, Success, Failure} import coulomb.RuntimeUnit import coulomb.rational.Rational -abstract class RuntimeUnitParser: +trait RuntimeUnitParser: def parse(expr: String): Either[String, RuntimeUnit] def render(u: RuntimeUnit): String diff --git a/pureconfig/src/main/scala/coulomb/pureconfig.scala b/pureconfig/src/main/scala/coulomb/pureconfig.scala index dfcb47cf3..9f3b9779f 100644 --- a/pureconfig/src/main/scala/coulomb/pureconfig.scala +++ b/pureconfig/src/main/scala/coulomb/pureconfig.scala @@ -132,3 +132,27 @@ object quantity: ConfigWriter[RuntimeQuantity[V]].contramap[Quantity[V, U]] { q => RuntimeQuantity(q.value, RuntimeUnit.of[U]) } + +class PureconfigRuntime(cr: CoefficientRuntime, rup: RuntimeUnitParser) + extends CoefficientRuntime + with RuntimeUnitParser: + + def parse(expr: String): Either[String, RuntimeUnit] = + rup.parse(expr) + def render(u: RuntimeUnit): String = + rup.render(u) + + def coefficientRational( + uf: RuntimeUnit, + ut: RuntimeUnit + ): Either[String, Rational] = + cr.coefficientRational(uf, ut) + +object PureconfigRuntime: + import coulomb.conversion.runtimes.mapping.MappingCoefficientRuntime + import coulomb.parser.standard.RuntimeUnitDslParser + + inline def of[UTL <: Tuple]: PureconfigRuntime = + val r = MappingCoefficientRuntime.of[UTL] + val p = RuntimeUnitDslParser.of[UTL] + new PureconfigRuntime(r, p) diff --git a/runtime/src/main/scala/coulomb/runtime/runtime.scala b/runtime/src/main/scala/coulomb/runtime/runtime.scala index 81ca706c6..2d92e5f2e 100644 --- a/runtime/src/main/scala/coulomb/runtime/runtime.scala +++ b/runtime/src/main/scala/coulomb/runtime/runtime.scala @@ -125,7 +125,7 @@ object RuntimeQuantity: add: RuntimeAdd[VL, VR] ): Either[String, RuntimeQuantity[add.VO]] = add.eval(ql, qr) -abstract class CoefficientRuntime: +trait CoefficientRuntime: def coefficientRational( uf: RuntimeUnit, ut: RuntimeUnit From d13e28345f37ae0a575c3cde97aaa007e619feea Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 6 Aug 2023 14:58:18 -0700 Subject: [PATCH 097/141] bump tlVersionIntroduced to 0.8.0 --- build.sbt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.sbt b/build.sbt index da5a19f3a..5187d5059 100644 --- a/build.sbt +++ b/build.sbt @@ -70,7 +70,7 @@ lazy val runtime = crossProject(JVMPlatform, JSPlatform, NativePlatform) units % Test ) .settings( - tlVersionIntroduced := Map("3" -> "0.7.4") + tlVersionIntroduced := Map("3" -> "0.8.0") ) .settings(commonSettings: _*) .settings( @@ -93,7 +93,7 @@ lazy val parser = crossProject(JVMPlatform, JSPlatform, NativePlatform) units % Test ) .settings( - tlVersionIntroduced := Map("3" -> "0.7.4") + tlVersionIntroduced := Map("3" -> "0.8.0") ) .settings(commonSettings: _*) .settings( @@ -111,7 +111,7 @@ lazy val pureconfig = crossProject(JVMPlatform, JSPlatform, NativePlatform) units % Test ) .settings( - tlVersionIntroduced := Map("3" -> "0.7.4") + tlVersionIntroduced := Map("3" -> "0.8.0") ) .settings(commonSettings: _*) .settings( From f604b7596b67d8710f3cf0d3b2296ef9be4b441d Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Tue, 8 Aug 2023 16:50:27 -0700 Subject: [PATCH 098/141] factor into io and policy --- pureconfig/src/main/scala/coulomb/io.scala | 128 ++++++++++++++++++ .../src/main/scala/coulomb/policy.scala | 22 +++ .../src/main/scala/coulomb/pureconfig.scala | 112 +-------------- 3 files changed, 151 insertions(+), 111 deletions(-) create mode 100644 pureconfig/src/main/scala/coulomb/io.scala create mode 100644 pureconfig/src/main/scala/coulomb/policy.scala diff --git a/pureconfig/src/main/scala/coulomb/io.scala b/pureconfig/src/main/scala/coulomb/io.scala new file mode 100644 index 000000000..2b9a5005c --- /dev/null +++ b/pureconfig/src/main/scala/coulomb/io.scala @@ -0,0 +1,128 @@ +/* + * Copyright 2022 Erik Erlandson + * + * 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 coulomb.pureconfig.io + +import scala.jdk.CollectionConverters.* +import _root_.pureconfig.* + +import algebra.ring.MultiplicativeSemigroup + +import coulomb.{infra => _, *} +import coulomb.syntax.* +import coulomb.rational.Rational +import coulomb.conversion.ValueConversion + +import scala.util.{Try, Success, Failure} + +import _root_.pureconfig.error.CannotConvert + +import coulomb.parser.RuntimeUnitParser + +import com.typesafe.config.{ConfigValue, ConfigValueFactory} + +object testing: + // probably useful for unit testing, will keep them here for now + extension [V, U](q: Quantity[V, U]) + inline def toCV(using ConfigWriter[Quantity[V, U]]): ConfigValue = + ConfigWriter[Quantity[V, U]].to(q) + extension (conf: ConfigValue) + inline def toQuantity[V, U](using ConfigReader[Quantity[V, U]]): Quantity[V, U] = + ConfigReader[Quantity[V, U]].from(conf).toSeq.head + +object rational: + given ctx_RationalReader: ConfigReader[Rational] = + ConfigReader[BigInt].map(Rational(_, 1)) `orElse` + ConfigReader[Double].map(Rational(_)) `orElse` + ConfigReader.forProduct2("n", "d") { (n: BigInt, d: BigInt) => + Rational(n, d) + } + + extension (v: BigInt) + def toCV(using + ConfigWriter[Int], + ConfigWriter[BigInt] + ): ConfigValue = + if (v.isValidInt) ConfigWriter[Int].to(v.toInt) + else ConfigWriter[BigInt].to(v) + + given ctx_RationalWriter(using + ConfigWriter[Int], + ConfigWriter[BigInt] + ): ConfigWriter[Rational] = + ConfigWriter.fromFunction[Rational] { r => + if (r.d == 1) + ConfigValueFactory.fromAnyRef(r.n.toCV) + else + ConfigValueFactory.fromAnyRef( + Map("n" -> r.n.toCV, "d" -> r.d.toCV).asJava + ) + } + +object ruDSL: + given ctx_RuntimeUnit_Reader(using + parser: RuntimeUnitParser + ): ConfigReader[RuntimeUnit] = + ConfigReader.fromCursor[RuntimeUnit] { cur => + cur.asString.flatMap { expr => + parser.parse(expr) match + case Right(u) => Right(u) + case Left(e) => cur.failed(CannotConvert(expr, "RuntimeUnit", e)) + } + } + + given ctx_RuntimeUnit_Writer(using + parser: RuntimeUnitParser + ): ConfigWriter[RuntimeUnit] = + ConfigWriter[String].contramap[RuntimeUnit] { u => + parser.render(u) + } + +object quantity: + given ctx_RuntimeQuantity_Reader[V](using + ConfigReader[V], + ConfigReader[RuntimeUnit] + ): ConfigReader[RuntimeQuantity[V]] = + ConfigReader.forProduct2("value", "unit") { (v: V, u: RuntimeUnit) => + RuntimeQuantity(v, u) + } + + given ctx_RuntimeQuantity_Writer[V](using + ConfigWriter[V], + ConfigWriter[RuntimeUnit] + ): ConfigWriter[RuntimeQuantity[V]] = + ConfigWriter.forProduct2("value", "unit") { (q: RuntimeQuantity[V]) => + (q.value, q.unit) + } + + inline given ctx_Quantity_Reader[V, U](using + crq: ConfigReader[RuntimeQuantity[V]], + crt: CoefficientRuntime, + vcr: ValueConversion[Rational, V], + mul: MultiplicativeSemigroup[V] + ): ConfigReader[Quantity[V, U]] = + ConfigReader[RuntimeQuantity[V]].emap { rq => + crt.coefficient[V](rq.unit, RuntimeUnit.of[U]) match + case Right(coef) => Right(mul.times(coef, rq.value).withUnit[U]) + case Left(e) => Left(CannotConvert(s"$rq", "Quantity", e)) + } + + inline given ctx_Quantity_Writer[V, U](using + ConfigWriter[RuntimeQuantity[V]] + ): ConfigWriter[Quantity[V, U]] = + ConfigWriter[RuntimeQuantity[V]].contramap[Quantity[V, U]] { q => + RuntimeQuantity(q.value, RuntimeUnit.of[U]) + } diff --git a/pureconfig/src/main/scala/coulomb/policy.scala b/pureconfig/src/main/scala/coulomb/policy.scala new file mode 100644 index 000000000..e921f4e88 --- /dev/null +++ b/pureconfig/src/main/scala/coulomb/policy.scala @@ -0,0 +1,22 @@ +/* + * Copyright 2022 Erik Erlandson + * + * 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 coulomb.pureconfig.policy + +object DSL: + export coulomb.pureconfig.io.rational.given + export coulomb.pureconfig.io.ruDSL.given + export coulomb.pureconfig.io.quantity.given diff --git a/pureconfig/src/main/scala/coulomb/pureconfig.scala b/pureconfig/src/main/scala/coulomb/pureconfig.scala index 9f3b9779f..18fc0c985 100644 --- a/pureconfig/src/main/scala/coulomb/pureconfig.scala +++ b/pureconfig/src/main/scala/coulomb/pureconfig.scala @@ -16,123 +16,13 @@ package coulomb.pureconfig -import scala.jdk.CollectionConverters.* -import _root_.pureconfig.* - -import algebra.ring.MultiplicativeSemigroup - import coulomb.{infra => _, *} import coulomb.syntax.* -import coulomb.rational.Rational -import coulomb.conversion.ValueConversion -import scala.util.{Try, Success, Failure} - -import _root_.pureconfig.error.CannotConvert +import coulomb.rational.Rational import coulomb.parser.RuntimeUnitParser -import com.typesafe.config.{ConfigValue, ConfigValueFactory} - -object policy: - object DSL: - export _root_.coulomb.pureconfig.rational.given - export _root_.coulomb.pureconfig.ruDSL.given - export _root_.coulomb.pureconfig.quantity.given - -object testing: - // probably useful for unit testing, will keep them here for now - extension [V, U](q: Quantity[V, U]) - inline def toCV(using ConfigWriter[Quantity[V, U]]): ConfigValue = - ConfigWriter[Quantity[V, U]].to(q) - extension (conf: ConfigValue) - inline def toQuantity[V, U](using ConfigReader[Quantity[V, U]]): Quantity[V, U] = - ConfigReader[Quantity[V, U]].from(conf).toSeq.head - -object rational: - given ctx_RationalReader: ConfigReader[Rational] = - ConfigReader[BigInt].map(Rational(_, 1)) `orElse` - ConfigReader[Double].map(Rational(_)) `orElse` - ConfigReader.forProduct2("n", "d") { (n: BigInt, d: BigInt) => - Rational(n, d) - } - - extension (v: BigInt) - def toCV(using - ConfigWriter[Int], - ConfigWriter[BigInt] - ): ConfigValue = - if (v.isValidInt) ConfigWriter[Int].to(v.toInt) - else ConfigWriter[BigInt].to(v) - - given ctx_RationalWriter(using - ConfigWriter[Int], - ConfigWriter[BigInt] - ): ConfigWriter[Rational] = - ConfigWriter.fromFunction[Rational] { r => - if (r.d == 1) - ConfigValueFactory.fromAnyRef(r.n.toCV) - else - ConfigValueFactory.fromAnyRef( - Map("n" -> r.n.toCV, "d" -> r.d.toCV).asJava - ) - } - -object ruDSL: - given ctx_RuntimeUnit_Reader(using - parser: RuntimeUnitParser - ): ConfigReader[RuntimeUnit] = - ConfigReader.fromCursor[RuntimeUnit] { cur => - cur.asString.flatMap { expr => - parser.parse(expr) match - case Right(u) => Right(u) - case Left(e) => cur.failed(CannotConvert(expr, "RuntimeUnit", e)) - } - } - - given ctx_RuntimeUnit_Writer(using - parser: RuntimeUnitParser - ): ConfigWriter[RuntimeUnit] = - ConfigWriter[String].contramap[RuntimeUnit] { u => - parser.render(u) - } - -object quantity: - given ctx_RuntimeQuantity_Reader[V](using - ConfigReader[V], - ConfigReader[RuntimeUnit] - ): ConfigReader[RuntimeQuantity[V]] = - ConfigReader.forProduct2("value", "unit") { (v: V, u: RuntimeUnit) => - RuntimeQuantity(v, u) - } - - given ctx_RuntimeQuantity_Writer[V](using - ConfigWriter[V], - ConfigWriter[RuntimeUnit] - ): ConfigWriter[RuntimeQuantity[V]] = - ConfigWriter.forProduct2("value", "unit") { (q: RuntimeQuantity[V]) => - (q.value, q.unit) - } - - inline given ctx_Quantity_Reader[V, U](using - crq: ConfigReader[RuntimeQuantity[V]], - crt: CoefficientRuntime, - vcr: ValueConversion[Rational, V], - mul: MultiplicativeSemigroup[V] - ): ConfigReader[Quantity[V, U]] = - ConfigReader[RuntimeQuantity[V]].emap { rq => - crt.coefficient[V](rq.unit, RuntimeUnit.of[U]) match - case Right(coef) => Right(mul.times(coef, rq.value).withUnit[U]) - case Left(e) => Left(CannotConvert(s"$rq", "Quantity", e)) - } - - inline given ctx_Quantity_Writer[V, U](using - ConfigWriter[RuntimeQuantity[V]] - ): ConfigWriter[Quantity[V, U]] = - ConfigWriter[RuntimeQuantity[V]].contramap[Quantity[V, U]] { q => - RuntimeQuantity(q.value, RuntimeUnit.of[U]) - } - class PureconfigRuntime(cr: CoefficientRuntime, rup: RuntimeUnitParser) extends CoefficientRuntime with RuntimeUnitParser: From e7f347a5a5e35dcb9b6d1b044a74905e6d0d164d Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Fri, 11 Aug 2023 17:08:45 -0700 Subject: [PATCH 099/141] skeleton unit testing --- .../src/test/scala/coulomb/quantity.scala | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 pureconfig/src/test/scala/coulomb/quantity.scala diff --git a/pureconfig/src/test/scala/coulomb/quantity.scala b/pureconfig/src/test/scala/coulomb/quantity.scala new file mode 100644 index 000000000..312aa8a9e --- /dev/null +++ b/pureconfig/src/test/scala/coulomb/quantity.scala @@ -0,0 +1,51 @@ +/* + * Copyright 2022 Erik Erlandson + * + * 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. + */ + +import coulomb.testing.CoulombSuite + +class QuantityDSLSuite extends CoulombSuite: + import pureconfig.{*, given} + + import coulomb.* + import coulomb.syntax.* + + import coulomb.policy.standard.given + import algebra.instances.all.given + import coulomb.ops.algebra.all.given + + import coulomb.pureconfig.* + import coulomb.pureconfig.policy.DSL.given + + import coulomb.units.si.{*, given} + import coulomb.units.si.prefixes.{*, given} + + given given_pureconfig: PureconfigRuntime = + PureconfigRuntime.of[ + "coulomb.units.si" *: + "coulomb.units.si.prefixes" *: + EmptyTuple + ] + + test("smoke test") { + ConfigSource.string("""{ value: 3, unit: kilometer}""") + .load[Quantity[Float, Meter]] + .assertRQ[Float, Meter](3000f) + + ConfigSource.string("""{ value: 3, unit: kilometer}""") + .load[Quantity[Float, Second]] + .assertL + } + From 1bf3a8c2d3419cbc5beec9043a45afb08cf98941 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Fri, 11 Aug 2023 17:31:05 -0700 Subject: [PATCH 100/141] add parser and pureconfig to root proj --- build.sbt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 5187d5059..755f2fae6 100644 --- a/build.sbt +++ b/build.sbt @@ -38,7 +38,17 @@ def commonSettings = Seq( ) lazy val root = tlCrossRootProject - .aggregate(core, units, runtime, spire, refined, testkit, unidocs) + .aggregate( + core, + units, + runtime, + parser, + pureconfig, + spire, + refined, + testkit, + unidocs + ) lazy val core = crossProject(JVMPlatform, JSPlatform, NativePlatform) .crossType(CrossType.Pure) From c7d3fd399d3df47286bcf04492c1c0e14a9e441a Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Fri, 11 Aug 2023 17:31:20 -0700 Subject: [PATCH 101/141] scalafmt updates --- .../src/main/scala/coulomb/parser/dsl.scala | 85 +++++++++++++------ .../scala/coulomb/parser/infra/meta.scala | 8 +- pureconfig/src/main/scala/coulomb/io.scala | 23 ++--- .../src/main/scala/coulomb/pureconfig.scala | 2 +- .../src/test/scala/coulomb/quantity.scala | 11 ++- 5 files changed, 84 insertions(+), 45 deletions(-) diff --git a/parser/src/main/scala/coulomb/parser/dsl.scala b/parser/src/main/scala/coulomb/parser/dsl.scala index 7d32897a9..2ec0a0f7c 100644 --- a/parser/src/main/scala/coulomb/parser/dsl.scala +++ b/parser/src/main/scala/coulomb/parser/dsl.scala @@ -22,11 +22,19 @@ import coulomb.RuntimeUnit import coulomb.rational.Rational object dsl: - def parser(unames: Map[String, String], pnames: Set[String], unamesinv: Map[String, String]): (String => Either[String, RuntimeUnit]) = - val p = catsparse.unit(catsparse.named(unames, pnames), catsparse.typed(unamesinv)) - (expr: String) => p.parse(expr) match - case Right((_, u)) => Right(u) - case Left(e) => Left(s"$e") + def parser( + unames: Map[String, String], + pnames: Set[String], + unamesinv: Map[String, String] + ): (String => Either[String, RuntimeUnit]) = + val p = catsparse.unit( + catsparse.named(unames, pnames), + catsparse.typed(unamesinv) + ) + (expr: String) => + p.parse(expr) match + case Right((_, u)) => Right(u) + case Left(e) => Left(s"$e") // parsing library is implementation detail // note this is private and so could be swapped out without breaking binary compat @@ -46,7 +54,9 @@ object dsl: case fplit(v) => Parser.pure(RuntimeUnit.UnitConst(Rational(v))) case _ => - Parser.failWith[RuntimeUnit](s"bad numeric literal '$lit'") + Parser.failWith[RuntimeUnit]( + s"bad numeric literal '$lit'" + ) } object intlit: @@ -71,7 +81,9 @@ object dsl: // scala identifier val idlit: Parser[String] = - (Rfc5234.alpha ~ (Rfc5234.alpha | Rfc5234.digit | Parser.char('$')).rep0).string + (Rfc5234.alpha ~ (Rfc5234.alpha | Rfc5234.digit | Parser.char( + '$' + )).rep0).string // fully qualified scala module path for a UnitType val typelit: Parser[String] = @@ -81,7 +93,7 @@ object dsl: // used for left-factoring the parsing for sequences of mul and div val muldivop: Parser[(RuntimeUnit, RuntimeUnit) => RuntimeUnit] = (Parser.char('*') <* ws0).as(RuntimeUnit.Mul(_, _)) | - (Parser.char('/') <* ws0).as(RuntimeUnit.Div(_, _)) + (Parser.char('/') <* ws0).as(RuntimeUnit.Div(_, _)) // used for left-factoring the parsing of "^" (power) val powop: Parser[(RuntimeUnit, RuntimeUnit) => RuntimeUnit] = @@ -91,19 +103,22 @@ object dsl: RuntimeUnit.Pow(b, e.toRational.toSeq.head) } - def unit(named: Parser[RuntimeUnit], typed: Parser[RuntimeUnit]): Parser[RuntimeUnit] = + def unit( + named: Parser[RuntimeUnit], + typed: Parser[RuntimeUnit] + ): Parser[RuntimeUnit] = lazy val unitexpr: Parser[RuntimeUnit] = Parser.defer { // sequence of mul and div operators // these have lowest precedence and form the top of the parse tree // example: * / * ... lazy val muldiv: Parser[RuntimeUnit] = chainl1(pow, muldivop) - + // powers of form ^ , where: // may be any unit expression and // is an expression that must evaluate to a valid numeric constant lazy val pow: Parser[RuntimeUnit] = - binaryl1(atom, numeric, powop) + binaryl1(atom, numeric, powop) // numeric literal, named unit, or sub-expr in parens lazy val atom: Parser[RuntimeUnit] = @@ -111,7 +126,10 @@ object dsl: // any unit subexpression inside of parens: () lazy val paren: Parser[RuntimeUnit] = - unitexpr.between(Parser.char('(') <* ws0, Parser.char(')') <* ws0) + unitexpr.between( + Parser.char('(') <* ws0, + Parser.char(')') <* ws0 + ) // parses a RuntimeUnit expression, but only succeeds // if it evaluates to a constant numeric value @@ -121,10 +139,10 @@ object dsl: lazy val numeric: Parser[RuntimeUnit] = atom.flatMap { u => u.toRational match - case Right(v) => - Parser.pure(RuntimeUnit.UnitConst(v)) - case Left(e) => - Parser.failWith[RuntimeUnit](e) + case Right(v) => + Parser.pure(RuntimeUnit.UnitConst(v)) + case Left(e) => + Parser.failWith[RuntimeUnit](e) } // return the top of the parse tree @@ -141,19 +159,26 @@ object dsl: // type paths are ok if they are in the map Parser.pure(RuntimeUnit.UnitType(path)) else - Parser.failWith[RuntimeUnit](s"unrecognized unit type '$path'") + Parser.failWith[RuntimeUnit]( + s"unrecognized unit type '$path'" + ) } // parses "raw" unit literals - only succeeds if the literal is // in the list of defined units (or unit prefixes) // these lists are intended to be constructed at compile-time via scala metaprogramming // to reduce errors - def named(unames: Map[String, String], pnames: Set[String]): Parser[RuntimeUnit] = + def named( + unames: Map[String, String], + pnames: Set[String] + ): Parser[RuntimeUnit] = val prefixunit: Parser[(String, String)] = if (pnames.isEmpty || unames.isEmpty) Parser.fail else - (strset(pnames) ~ strset(unames.keySet `diff` pnames)) <* Parser.end + (strset(pnames) ~ strset( + unames.keySet `diff` pnames + )) <* Parser.end unitlit.flatMap { name => if (unames.contains(name)) // name is a defined unit, return its type @@ -167,7 +192,9 @@ object dsl: val u = RuntimeUnit.UnitType(unames(un)) Parser.pure(RuntimeUnit.Mul(p, u)) case Left(_) => - Parser.failWith[RuntimeUnit](s"unrecognized unit '$name'") + Parser.failWith[RuntimeUnit]( + s"unrecognized unit '$name'" + ) } def strset(ss: Set[String]): Parser[String] = @@ -179,7 +206,8 @@ object dsl: // construct a parser "branch" for each starting character val hp = ss.map(_.head).toList.map { h => // set of string tails starting with char h - val tails = ss.filter(_.head == h).map(_.drop(1)).filter(_.length > 0) + val tails = + ss.filter(_.head == h).map(_.drop(1)).filter(_.length > 0) if (tails.isEmpty) // no remaining string tails, just parse char h Parser.char(h) @@ -197,7 +225,7 @@ object dsl: // https://github.com/j-mie6/design-patterns-for-parser-combinators#readme def chainl1[X](p: Parser[X], op: Parser[(X, X) => X]): Parser[X] = lazy val rest: Parser0[X => X] = Parser.defer0 { - val some: Parser0[X => X] = (op, p, rest).mapN { + val some: Parser0[X => X] = (op, p, rest).mapN { // found an , with possibly more (f, y, next) => ((x: X) => next(f(x, y))) } @@ -212,10 +240,14 @@ object dsl: // like chainl1 but specifically a single left-factored binary expr // [ ] - def binaryl1[X](pl: Parser[X], pr: Parser[X], op: Parser[(X, X) => X]): Parser[X] = + def binaryl1[X]( + pl: Parser[X], + pr: Parser[X], + op: Parser[(X, X) => X] + ): Parser[X] = val some: Parser0[X => X] = (op, pr).mapN { // found an - (f, y) => ((x: X) => f(x, y)) + (f, y) => ((x: X) => f(x, y)) } val none: Parser0[X => X] = Parser.pure(identity[X]) rapp(pl, some | none).asInstanceOf[Parser[X]] @@ -236,5 +268,6 @@ object dsl: extension [X1, X2, X3](p: (Parser0[X1], Parser0[X2], Parser0[X3])) def mapN[Z](f: (X1, X2, X3) => Z): Parser0[Z] = - ((p._1 ~ p._2) ~ p._3).map { case ((x1, x2), x3) => f(x1, x2, x3) } - + ((p._1 ~ p._2) ~ p._3).map { case ((x1, x2), x3) => + f(x1, x2, x3) + } diff --git a/parser/src/main/scala/coulomb/parser/infra/meta.scala b/parser/src/main/scala/coulomb/parser/infra/meta.scala index 6e65206a1..b803ef327 100644 --- a/parser/src/main/scala/coulomb/parser/infra/meta.scala +++ b/parser/src/main/scala/coulomb/parser/infra/meta.scala @@ -47,7 +47,9 @@ object meta: val pnames = ${ Expr(pn1) } } - private def collect(using Quotes)(tl: List[quotes.reflect.TypeRepr]): (Map[String, String], Set[String]) = + private def collect(using Quotes)( + tl: List[quotes.reflect.TypeRepr] + ): (Map[String, String], Set[String]) = import quotes.reflect.* tl match case Nil => (Map.empty[String, String], Set.empty[String]) @@ -69,7 +71,9 @@ object meta: // always add to unit types val unr = un + (name -> head.typeSymbol.fullName) // if it is derived from unitless, also add it to prefix unit set - val pnr = if (convertible(head, TypeRepr.of[1])) pn + name else pn + val pnr = + if (convertible(head, TypeRepr.of[1])) pn + name + else pn (unr, pnr) case _ => report.errorAndAbort(s"collect: bad type ${head.show}") diff --git a/pureconfig/src/main/scala/coulomb/io.scala b/pureconfig/src/main/scala/coulomb/io.scala index 2b9a5005c..264405d16 100644 --- a/pureconfig/src/main/scala/coulomb/io.scala +++ b/pureconfig/src/main/scala/coulomb/io.scala @@ -40,16 +40,18 @@ object testing: inline def toCV(using ConfigWriter[Quantity[V, U]]): ConfigValue = ConfigWriter[Quantity[V, U]].to(q) extension (conf: ConfigValue) - inline def toQuantity[V, U](using ConfigReader[Quantity[V, U]]): Quantity[V, U] = + inline def toQuantity[V, U](using + ConfigReader[Quantity[V, U]] + ): Quantity[V, U] = ConfigReader[Quantity[V, U]].from(conf).toSeq.head object rational: given ctx_RationalReader: ConfigReader[Rational] = ConfigReader[BigInt].map(Rational(_, 1)) `orElse` - ConfigReader[Double].map(Rational(_)) `orElse` - ConfigReader.forProduct2("n", "d") { (n: BigInt, d: BigInt) => - Rational(n, d) - } + ConfigReader[Double].map(Rational(_)) `orElse` + ConfigReader.forProduct2("n", "d") { (n: BigInt, d: BigInt) => + Rational(n, d) + } extension (v: BigInt) def toCV(using @@ -80,7 +82,8 @@ object ruDSL: cur.asString.flatMap { expr => parser.parse(expr) match case Right(u) => Right(u) - case Left(e) => cur.failed(CannotConvert(expr, "RuntimeUnit", e)) + case Left(e) => + cur.failed(CannotConvert(expr, "RuntimeUnit", e)) } } @@ -117,12 +120,12 @@ object quantity: ConfigReader[RuntimeQuantity[V]].emap { rq => crt.coefficient[V](rq.unit, RuntimeUnit.of[U]) match case Right(coef) => Right(mul.times(coef, rq.value).withUnit[U]) - case Left(e) => Left(CannotConvert(s"$rq", "Quantity", e)) + case Left(e) => Left(CannotConvert(s"$rq", "Quantity", e)) } inline given ctx_Quantity_Writer[V, U](using ConfigWriter[RuntimeQuantity[V]] ): ConfigWriter[Quantity[V, U]] = - ConfigWriter[RuntimeQuantity[V]].contramap[Quantity[V, U]] { q => - RuntimeQuantity(q.value, RuntimeUnit.of[U]) - } + ConfigWriter[RuntimeQuantity[V]].contramap[Quantity[V, U]] { q => + RuntimeQuantity(q.value, RuntimeUnit.of[U]) + } diff --git a/pureconfig/src/main/scala/coulomb/pureconfig.scala b/pureconfig/src/main/scala/coulomb/pureconfig.scala index 18fc0c985..7c3626854 100644 --- a/pureconfig/src/main/scala/coulomb/pureconfig.scala +++ b/pureconfig/src/main/scala/coulomb/pureconfig.scala @@ -26,7 +26,7 @@ import coulomb.parser.RuntimeUnitParser class PureconfigRuntime(cr: CoefficientRuntime, rup: RuntimeUnitParser) extends CoefficientRuntime with RuntimeUnitParser: - + def parse(expr: String): Either[String, RuntimeUnit] = rup.parse(expr) def render(u: RuntimeUnit): String = diff --git a/pureconfig/src/test/scala/coulomb/quantity.scala b/pureconfig/src/test/scala/coulomb/quantity.scala index 312aa8a9e..fba4c3003 100644 --- a/pureconfig/src/test/scala/coulomb/quantity.scala +++ b/pureconfig/src/test/scala/coulomb/quantity.scala @@ -34,18 +34,17 @@ class QuantityDSLSuite extends CoulombSuite: given given_pureconfig: PureconfigRuntime = PureconfigRuntime.of[ - "coulomb.units.si" *: - "coulomb.units.si.prefixes" *: - EmptyTuple + "coulomb.units.si" *: "coulomb.units.si.prefixes" *: EmptyTuple ] test("smoke test") { - ConfigSource.string("""{ value: 3, unit: kilometer}""") + ConfigSource + .string("""{ value: 3, unit: kilometer}""") .load[Quantity[Float, Meter]] .assertRQ[Float, Meter](3000f) - ConfigSource.string("""{ value: 3, unit: kilometer}""") + ConfigSource + .string("""{ value: 3, unit: kilometer}""") .load[Quantity[Float, Second]] .assertL } - From 6ff7f7bdeb85b7503521e344535bc1d06e74c8b1 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 12 Aug 2023 07:22:03 -0700 Subject: [PATCH 102/141] disable js and native platforms for parser and pureconfig --- .github/workflows/ci.yml | 4 ++-- build.sbt | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 78511f1d1..9ffe78f65 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,11 +80,11 @@ jobs: - name: Make target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/scala3') - run: mkdir -p runtime/.js/target spire/.js/target spire/.jvm/target benchmarks/target testkit/.native/target target pureconfig/.native/target all/target units/.jvm/target runtime/.native/target testkit/.js/target parser/.jvm/target unidocs/target .js/target parser/.js/target core/.native/target site/target pureconfig/.jvm/target spire/.native/target parser/.native/target core/.js/target units/.native/target runtime/.jvm/target core/.jvm/target refined/.native/target .jvm/target .native/target refined/.js/target refined/.jvm/target units/.js/target testkit/.jvm/target pureconfig/.js/target project/target + run: mkdir -p runtime/.js/target spire/.js/target spire/.jvm/target benchmarks/target testkit/.native/target target all/target units/.jvm/target runtime/.native/target testkit/.js/target parser/.jvm/target unidocs/target .js/target core/.native/target site/target pureconfig/.jvm/target spire/.native/target core/.js/target units/.native/target runtime/.jvm/target core/.jvm/target refined/.native/target .jvm/target .native/target refined/.js/target refined/.jvm/target units/.js/target testkit/.jvm/target project/target - name: Compress target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/scala3') - run: tar cf targets.tar runtime/.js/target spire/.js/target spire/.jvm/target benchmarks/target testkit/.native/target target pureconfig/.native/target all/target units/.jvm/target runtime/.native/target testkit/.js/target parser/.jvm/target unidocs/target .js/target parser/.js/target core/.native/target site/target pureconfig/.jvm/target spire/.native/target parser/.native/target core/.js/target units/.native/target runtime/.jvm/target core/.jvm/target refined/.native/target .jvm/target .native/target refined/.js/target refined/.jvm/target units/.js/target testkit/.jvm/target pureconfig/.js/target project/target + run: tar cf targets.tar runtime/.js/target spire/.js/target spire/.jvm/target benchmarks/target testkit/.native/target target all/target units/.jvm/target runtime/.native/target testkit/.js/target parser/.jvm/target unidocs/target .js/target core/.native/target site/target pureconfig/.jvm/target spire/.native/target core/.js/target units/.native/target runtime/.jvm/target core/.jvm/target refined/.native/target .jvm/target .native/target refined/.js/target refined/.jvm/target units/.js/target testkit/.jvm/target project/target - name: Upload target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/scala3') diff --git a/build.sbt b/build.sbt index 755f2fae6..729e23b3b 100644 --- a/build.sbt +++ b/build.sbt @@ -93,7 +93,8 @@ lazy val runtime = crossProject(JVMPlatform, JSPlatform, NativePlatform) Test / unmanagedSources / excludeFilter := HiddenFileFilter || "*stagingquantity.scala" ) -lazy val parser = crossProject(JVMPlatform, JSPlatform, NativePlatform) +// cats-parse doesn't seem to build for JS or Native +lazy val parser = crossProject(JVMPlatform /*, JSPlatform, NativePlatform */) .crossType(CrossType.Pure) .in(file("parser")) .settings(name := "coulomb-parser") @@ -110,7 +111,8 @@ lazy val parser = crossProject(JVMPlatform, JSPlatform, NativePlatform) libraryDependencies += "org.typelevel" %% "cats-parse" % "0.3.7" ) -lazy val pureconfig = crossProject(JVMPlatform, JSPlatform, NativePlatform) +// pureconfig doesn't seem to build for JS or Native +lazy val pureconfig = crossProject(JVMPlatform /*, JSPlatform, NativePlatform */) .crossType(CrossType.Pure) .in(file("pureconfig")) .settings(name := "coulomb-pureconfig") From a574952f6792b797fba4bf5517cf8b8ea95b4498 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 12 Aug 2023 07:22:26 -0700 Subject: [PATCH 103/141] testing skeleton for parser --- parser/src/test/scala/coulomb/dsl.scala | 35 +++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 parser/src/test/scala/coulomb/dsl.scala diff --git a/parser/src/test/scala/coulomb/dsl.scala b/parser/src/test/scala/coulomb/dsl.scala new file mode 100644 index 000000000..54d96612a --- /dev/null +++ b/parser/src/test/scala/coulomb/dsl.scala @@ -0,0 +1,35 @@ +/* + * Copyright 2022 Erik Erlandson + * + * 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. + */ + +import coulomb.testing.CoulombSuite + +class ParserDSLSuite extends CoulombSuite: + import coulomb.* + import coulomb.syntax.* + import coulomb.parser.RuntimeUnitParser + import coulomb.parser.standard.RuntimeUnitDslParser + + import coulomb.RuntimeUnit + + val dslparser: RuntimeUnitParser = + RuntimeUnitDslParser.of[ + "coulomb.units.si" *: "coulomb.units.si.prefixes" *: EmptyTuple + ] + + test("smoke test") { + val t = dslparser.parse("kilometer") + println(t) + } From 2666b4653447ee4c43e7c64c4007411135c3d6f9 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 12 Aug 2023 07:23:41 -0700 Subject: [PATCH 104/141] scalafmt --- build.sbt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 729e23b3b..4ef8d60ad 100644 --- a/build.sbt +++ b/build.sbt @@ -94,7 +94,7 @@ lazy val runtime = crossProject(JVMPlatform, JSPlatform, NativePlatform) ) // cats-parse doesn't seem to build for JS or Native -lazy val parser = crossProject(JVMPlatform /*, JSPlatform, NativePlatform */) +lazy val parser = crossProject(JVMPlatform /*, JSPlatform, NativePlatform */ ) .crossType(CrossType.Pure) .in(file("parser")) .settings(name := "coulomb-parser") @@ -112,7 +112,9 @@ lazy val parser = crossProject(JVMPlatform /*, JSPlatform, NativePlatform */) ) // pureconfig doesn't seem to build for JS or Native -lazy val pureconfig = crossProject(JVMPlatform /*, JSPlatform, NativePlatform */) +lazy val pureconfig = crossProject( + JVMPlatform /*, JSPlatform, NativePlatform */ +) .crossType(CrossType.Pure) .in(file("pureconfig")) .settings(name := "coulomb-pureconfig") From 496c19bea81949bc8e73468b6c4bd31a0f709630 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 12 Aug 2023 10:16:28 -0700 Subject: [PATCH 105/141] parser builds for JS and Native --- .github/workflows/ci.yml | 4 ++-- build.sbt | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9ffe78f65..db80af1f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,11 +80,11 @@ jobs: - name: Make target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/scala3') - run: mkdir -p runtime/.js/target spire/.js/target spire/.jvm/target benchmarks/target testkit/.native/target target all/target units/.jvm/target runtime/.native/target testkit/.js/target parser/.jvm/target unidocs/target .js/target core/.native/target site/target pureconfig/.jvm/target spire/.native/target core/.js/target units/.native/target runtime/.jvm/target core/.jvm/target refined/.native/target .jvm/target .native/target refined/.js/target refined/.jvm/target units/.js/target testkit/.jvm/target project/target + run: mkdir -p runtime/.js/target spire/.js/target spire/.jvm/target benchmarks/target testkit/.native/target target all/target units/.jvm/target runtime/.native/target testkit/.js/target parser/.jvm/target unidocs/target .js/target parser/.js/target core/.native/target site/target pureconfig/.jvm/target spire/.native/target parser/.native/target core/.js/target units/.native/target runtime/.jvm/target core/.jvm/target refined/.native/target .jvm/target .native/target refined/.js/target refined/.jvm/target units/.js/target testkit/.jvm/target project/target - name: Compress target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/scala3') - run: tar cf targets.tar runtime/.js/target spire/.js/target spire/.jvm/target benchmarks/target testkit/.native/target target all/target units/.jvm/target runtime/.native/target testkit/.js/target parser/.jvm/target unidocs/target .js/target core/.native/target site/target pureconfig/.jvm/target spire/.native/target core/.js/target units/.native/target runtime/.jvm/target core/.jvm/target refined/.native/target .jvm/target .native/target refined/.js/target refined/.jvm/target units/.js/target testkit/.jvm/target project/target + run: tar cf targets.tar runtime/.js/target spire/.js/target spire/.jvm/target benchmarks/target testkit/.native/target target all/target units/.jvm/target runtime/.native/target testkit/.js/target parser/.jvm/target unidocs/target .js/target parser/.js/target core/.native/target site/target pureconfig/.jvm/target spire/.native/target parser/.native/target core/.js/target units/.native/target runtime/.jvm/target core/.jvm/target refined/.native/target .jvm/target .native/target refined/.js/target refined/.jvm/target units/.js/target testkit/.jvm/target project/target - name: Upload target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/scala3') diff --git a/build.sbt b/build.sbt index 4ef8d60ad..001197b47 100644 --- a/build.sbt +++ b/build.sbt @@ -94,7 +94,7 @@ lazy val runtime = crossProject(JVMPlatform, JSPlatform, NativePlatform) ) // cats-parse doesn't seem to build for JS or Native -lazy val parser = crossProject(JVMPlatform /*, JSPlatform, NativePlatform */ ) +lazy val parser = crossProject(JVMPlatform, JSPlatform, NativePlatform) .crossType(CrossType.Pure) .in(file("parser")) .settings(name := "coulomb-parser") @@ -108,10 +108,11 @@ lazy val parser = crossProject(JVMPlatform /*, JSPlatform, NativePlatform */ ) ) .settings(commonSettings: _*) .settings( - libraryDependencies += "org.typelevel" %% "cats-parse" % "0.3.7" + libraryDependencies += "org.typelevel" %%% "cats-parse" % "0.3.10" ) -// pureconfig doesn't seem to build for JS or Native +// pureconfig doesn't currently build for JS or Native +// https://github.com/pureconfig/pureconfig/issues/1307 lazy val pureconfig = crossProject( JVMPlatform /*, JSPlatform, NativePlatform */ ) @@ -129,7 +130,7 @@ lazy val pureconfig = crossProject( ) .settings(commonSettings: _*) .settings( - libraryDependencies += "com.github.pureconfig" %% "pureconfig-core" % "0.17.2" + libraryDependencies += "com.github.pureconfig" %%% "pureconfig-core" % "0.17.4" ) lazy val spire = crossProject(JVMPlatform, JSPlatform, NativePlatform) From f0de8e5dd9468c84ee4ce1940e83f7573b83b0de Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 13 Aug 2023 13:07:19 -0700 Subject: [PATCH 106/141] wip json io for RuntimeUnit --- pureconfig/src/main/scala/coulomb/io.scala | 77 ++++++++++++++----- .../src/main/scala/coulomb/policy.scala | 5 ++ .../src/main/scala/coulomb/pureconfig.scala | 24 +++++- 3 files changed, 81 insertions(+), 25 deletions(-) diff --git a/pureconfig/src/main/scala/coulomb/io.scala b/pureconfig/src/main/scala/coulomb/io.scala index 264405d16..6a119da83 100644 --- a/pureconfig/src/main/scala/coulomb/io.scala +++ b/pureconfig/src/main/scala/coulomb/io.scala @@ -16,8 +16,14 @@ package coulomb.pureconfig.io +import scala.util.{Try, Success, Failure} + import scala.jdk.CollectionConverters.* + +import com.typesafe.config.{ConfigValue, ConfigValueFactory} + import _root_.pureconfig.* +import _root_.pureconfig.error.CannotConvert import algebra.ring.MultiplicativeSemigroup @@ -26,14 +32,8 @@ import coulomb.syntax.* import coulomb.rational.Rational import coulomb.conversion.ValueConversion -import scala.util.{Try, Success, Failure} - -import _root_.pureconfig.error.CannotConvert - import coulomb.parser.RuntimeUnitParser -import com.typesafe.config.{ConfigValue, ConfigValueFactory} - object testing: // probably useful for unit testing, will keep them here for now extension [V, U](q: Quantity[V, U]) @@ -46,25 +46,21 @@ object testing: ConfigReader[Quantity[V, U]].from(conf).toSeq.head object rational: - given ctx_RationalReader: ConfigReader[Rational] = + import _root_.pureconfig.{*, given} + + extension (v: BigInt) + def toCV: ConfigValue = + if (v.isValidInt) ConfigWriter[Int].to(v.toInt) + else ConfigWriter[BigInt].to(v) + + val reader: ConfigReader[Rational] = ConfigReader[BigInt].map(Rational(_, 1)) `orElse` ConfigReader[Double].map(Rational(_)) `orElse` ConfigReader.forProduct2("n", "d") { (n: BigInt, d: BigInt) => Rational(n, d) } - extension (v: BigInt) - def toCV(using - ConfigWriter[Int], - ConfigWriter[BigInt] - ): ConfigValue = - if (v.isValidInt) ConfigWriter[Int].to(v.toInt) - else ConfigWriter[BigInt].to(v) - - given ctx_RationalWriter(using - ConfigWriter[Int], - ConfigWriter[BigInt] - ): ConfigWriter[Rational] = + val writer: ConfigWriter[Rational] = ConfigWriter.fromFunction[Rational] { r => if (r.d == 1) ConfigValueFactory.fromAnyRef(r.n.toCV) @@ -74,8 +70,14 @@ object rational: ) } + given ctx_RationalReader: ConfigReader[Rational] = + reader + + given ctx_RationalWriter: ConfigWriter[Rational] = + writer + object ruDSL: - given ctx_RuntimeUnit_Reader(using + given ctx_RuntimeUnit_DSL_Reader(using parser: RuntimeUnitParser ): ConfigReader[RuntimeUnit] = ConfigReader.fromCursor[RuntimeUnit] { cur => @@ -87,13 +89,46 @@ object ruDSL: } } - given ctx_RuntimeUnit_Writer(using + given ctx_RuntimeUnit_DSL_Writer(using parser: RuntimeUnitParser ): ConfigWriter[RuntimeUnit] = ConfigWriter[String].contramap[RuntimeUnit] { u => parser.render(u) } +object ruJSON: + import coulomb.pureconfig.UnitPathMapper + + given ctx_RuntimeUnit_JSON_Reader(using + rr: ConfigReader[Rational], + upm: UnitPathMapper + ): ConfigReader[RuntimeUnit] = + ConfigReader[Rational].map(RuntimeUnit.UnitConst(_)) + `orElse` ConfigReader[String].emap { id => + upm.path(id) match + case Right(path) => Right(RuntimeUnit.UnitType(path)) + case Left(e) => + Left(CannotConvert(s"$id", "RuntimeUnit", e)) + } + `orElse` ConfigReader + .forProduct3("lhs", "op", "rhs") { + (lhs: RuntimeUnit, op: String, rhs: RuntimeUnit) => + (lhs, op, rhs) + } + .emap { (lhs, op, rhs) => + op match + case "*" => Right(RuntimeUnit.Mul(lhs, rhs)) + case "/" => Right(RuntimeUnit.Div(lhs, rhs)) + case _ => + Left( + CannotConvert( + s"${(lhs, op, rhs)}", + "RuntimeUnit", + s"unrecognized operator: '$op'" + ) + ) + } + object quantity: given ctx_RuntimeQuantity_Reader[V](using ConfigReader[V], diff --git a/pureconfig/src/main/scala/coulomb/policy.scala b/pureconfig/src/main/scala/coulomb/policy.scala index e921f4e88..c72b45012 100644 --- a/pureconfig/src/main/scala/coulomb/policy.scala +++ b/pureconfig/src/main/scala/coulomb/policy.scala @@ -20,3 +20,8 @@ object DSL: export coulomb.pureconfig.io.rational.given export coulomb.pureconfig.io.ruDSL.given export coulomb.pureconfig.io.quantity.given + +object JSON: + export coulomb.pureconfig.io.rational.given + export coulomb.pureconfig.io.ruJSON.given + export coulomb.pureconfig.io.quantity.given diff --git a/pureconfig/src/main/scala/coulomb/pureconfig.scala b/pureconfig/src/main/scala/coulomb/pureconfig.scala index 7c3626854..a1556bc3b 100644 --- a/pureconfig/src/main/scala/coulomb/pureconfig.scala +++ b/pureconfig/src/main/scala/coulomb/pureconfig.scala @@ -23,9 +23,16 @@ import coulomb.rational.Rational import coulomb.parser.RuntimeUnitParser -class PureconfigRuntime(cr: CoefficientRuntime, rup: RuntimeUnitParser) - extends CoefficientRuntime - with RuntimeUnitParser: +trait UnitPathMapper: + def path(expr: String): Either[String, String] + +class PureconfigRuntime( + cr: CoefficientRuntime, + rup: RuntimeUnitParser, + upm: String => Either[String, String] +) extends CoefficientRuntime + with RuntimeUnitParser + with UnitPathMapper: def parse(expr: String): Either[String, RuntimeUnit] = rup.parse(expr) @@ -38,6 +45,9 @@ class PureconfigRuntime(cr: CoefficientRuntime, rup: RuntimeUnitParser) ): Either[String, Rational] = cr.coefficientRational(uf, ut) + def path(expr: String): Either[String, String] = + upm(expr) + object PureconfigRuntime: import coulomb.conversion.runtimes.mapping.MappingCoefficientRuntime import coulomb.parser.standard.RuntimeUnitDslParser @@ -45,4 +55,10 @@ object PureconfigRuntime: inline def of[UTL <: Tuple]: PureconfigRuntime = val r = MappingCoefficientRuntime.of[UTL] val p = RuntimeUnitDslParser.of[UTL] - new PureconfigRuntime(r, p) + val u: (String => Either[String, String]) = (expr: String) => { + if (expr.isEmpty) Left(expr) + else if (expr.head == '@') Right(expr.tail) + else if (p.unames.contains(expr)) Right(p.unames(expr)) + else Left(expr) + } + new PureconfigRuntime(r, p, u) From 28925f4fff80bdace3466d53c67c9273e15aaadb Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 13 Aug 2023 15:44:31 -0700 Subject: [PATCH 107/141] smoke test JSON RuntimeUnit reader --- pureconfig/src/main/scala/coulomb/io.scala | 31 +++++++++--------- .../src/test/scala/coulomb/quantity.scala | 32 +++++++++++++++++-- 2 files changed, 45 insertions(+), 18 deletions(-) diff --git a/pureconfig/src/main/scala/coulomb/io.scala b/pureconfig/src/main/scala/coulomb/io.scala index 6a119da83..e144c64cc 100644 --- a/pureconfig/src/main/scala/coulomb/io.scala +++ b/pureconfig/src/main/scala/coulomb/io.scala @@ -46,21 +46,20 @@ object testing: ConfigReader[Quantity[V, U]].from(conf).toSeq.head object rational: - import _root_.pureconfig.{*, given} - extension (v: BigInt) def toCV: ConfigValue = if (v.isValidInt) ConfigWriter[Int].to(v.toInt) else ConfigWriter[BigInt].to(v) - val reader: ConfigReader[Rational] = - ConfigReader[BigInt].map(Rational(_, 1)) `orElse` - ConfigReader[Double].map(Rational(_)) `orElse` - ConfigReader.forProduct2("n", "d") { (n: BigInt, d: BigInt) => - Rational(n, d) + given ctx_RationalReader: ConfigReader[Rational] = + ConfigReader[BigInt].map(Rational(_, 1)) + `orElse` ConfigReader[Double].map(Rational(_)) + `orElse` ConfigReader.forProduct2("n", "d") { + (n: BigInt, d: BigInt) => + Rational(n, d) } - val writer: ConfigWriter[Rational] = + given ctx_RationalWriter: ConfigWriter[Rational] = ConfigWriter.fromFunction[Rational] { r => if (r.d == 1) ConfigValueFactory.fromAnyRef(r.n.toCV) @@ -70,12 +69,6 @@ object rational: ) } - given ctx_RationalReader: ConfigReader[Rational] = - reader - - given ctx_RationalWriter: ConfigWriter[Rational] = - writer - object ruDSL: given ctx_RuntimeUnit_DSL_Reader(using parser: RuntimeUnitParser @@ -107,8 +100,14 @@ object ruJSON: `orElse` ConfigReader[String].emap { id => upm.path(id) match case Right(path) => Right(RuntimeUnit.UnitType(path)) - case Left(e) => - Left(CannotConvert(s"$id", "RuntimeUnit", e)) + case Left(_) => + Left( + CannotConvert( + s"$id", + "RuntimeUnit", + s"id has no mapping: '$id'" + ) + ) } `orElse` ConfigReader .forProduct3("lhs", "op", "rhs") { diff --git a/pureconfig/src/test/scala/coulomb/quantity.scala b/pureconfig/src/test/scala/coulomb/quantity.scala index fba4c3003..9a89236e5 100644 --- a/pureconfig/src/test/scala/coulomb/quantity.scala +++ b/pureconfig/src/test/scala/coulomb/quantity.scala @@ -39,12 +39,40 @@ class QuantityDSLSuite extends CoulombSuite: test("smoke test") { ConfigSource - .string("""{ value: 3, unit: kilometer}""") + .string("""{value: 3, unit: kilometer}""") .load[Quantity[Float, Meter]] .assertRQ[Float, Meter](3000f) ConfigSource - .string("""{ value: 3, unit: kilometer}""") + .string("""{value: 3, unit: kilometer}""") .load[Quantity[Float, Second]] .assertL } + +class QuantityJSONSuite extends CoulombSuite: + import pureconfig.{*, given} + + import coulomb.* + import coulomb.syntax.* + + import coulomb.policy.standard.given + import algebra.instances.all.given + import coulomb.ops.algebra.all.given + + import coulomb.pureconfig.* + import coulomb.pureconfig.policy.JSON.given + + import coulomb.units.si.{*, given} + import coulomb.units.si.prefixes.{*, given} + + given given_pureconfig: PureconfigRuntime = + PureconfigRuntime.of[ + "coulomb.units.si" *: "coulomb.units.si.prefixes" *: EmptyTuple + ] + + test("smoke test") { + ConfigSource + .string("""{value: 3, unit: {lhs: kilo, op: "*", rhs: meter}}""") + .load[Quantity[Float, Meter]] + .assertRQ[Float, Meter](3000f) + } From 8fef20dd2a9f4ec5ebf32b3cabfe64ae3e8a82c9 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Mon, 14 Aug 2023 07:29:42 -0700 Subject: [PATCH 108/141] add exponent op to json reader --- pureconfig/src/main/scala/coulomb/io.scala | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pureconfig/src/main/scala/coulomb/io.scala b/pureconfig/src/main/scala/coulomb/io.scala index e144c64cc..ee6f5cf95 100644 --- a/pureconfig/src/main/scala/coulomb/io.scala +++ b/pureconfig/src/main/scala/coulomb/io.scala @@ -118,6 +118,17 @@ object ruJSON: op match case "*" => Right(RuntimeUnit.Mul(lhs, rhs)) case "/" => Right(RuntimeUnit.Div(lhs, rhs)) + case "^" => + rhs.toRational match + case Right(e) => Right(RuntimeUnit.Pow(lhs, e)) + case Left(_) => + Left( + CannotConvert( + s"${(lhs, op, rhs)}", + "RuntimeUnit", + s"bad exponent '$rhs'" + ) + ) case _ => Left( CannotConvert( From f575c156c5e4b808764378d72c6aad76cbef9f89 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Mon, 14 Aug 2023 15:59:35 -0700 Subject: [PATCH 109/141] json writer for runtime unit --- pureconfig/src/main/scala/coulomb/io.scala | 40 ++++++++++++++++++- .../src/main/scala/coulomb/pureconfig.scala | 34 +++++++++++----- 2 files changed, 62 insertions(+), 12 deletions(-) diff --git a/pureconfig/src/main/scala/coulomb/io.scala b/pureconfig/src/main/scala/coulomb/io.scala index ee6f5cf95..c20e6f152 100644 --- a/pureconfig/src/main/scala/coulomb/io.scala +++ b/pureconfig/src/main/scala/coulomb/io.scala @@ -90,7 +90,7 @@ object ruDSL: } object ruJSON: - import coulomb.pureconfig.UnitPathMapper + import coulomb.pureconfig.{UnitPathMapper, PathUnitMapper} given ctx_RuntimeUnit_JSON_Reader(using rr: ConfigReader[Rational], @@ -139,6 +139,44 @@ object ruJSON: ) } + given ctx_RuntimeUnit_JSON_Writer(using + cwr: ConfigWriter[Rational], + pum: PathUnitMapper + ): ConfigWriter[RuntimeUnit] = + def u2cv(u: RuntimeUnit): ConfigValue = + u match + case RuntimeUnit.UnitConst(v) => + ConfigValueFactory.fromAnyRef(ConfigWriter[Rational].to(v)) + case RuntimeUnit.UnitType(path) => + ConfigValueFactory.fromAnyRef( + ConfigWriter[String].to(pum.unit(path)) + ) + case RuntimeUnit.Mul(lhs, rhs) => + ConfigValueFactory.fromAnyRef( + Map( + "lhs" -> u2cv(lhs), + "rhs" -> u2cv(rhs), + "op" -> ConfigWriter[String].to("*") + ).asJava + ) + case RuntimeUnit.Div(num, den) => + ConfigValueFactory.fromAnyRef( + Map( + "lhs" -> u2cv(num), + "rhs" -> u2cv(den), + "op" -> ConfigWriter[String].to("/") + ).asJava + ) + case RuntimeUnit.Pow(b, e) => + ConfigValueFactory.fromAnyRef( + Map( + "lhs" -> u2cv(b), + "rhs" -> ConfigWriter[Rational].to(e), + "op" -> ConfigWriter[String].to("^") + ).asJava + ) + ConfigWriter.fromFunction[RuntimeUnit](u2cv) + object quantity: given ctx_RuntimeQuantity_Reader[V](using ConfigReader[V], diff --git a/pureconfig/src/main/scala/coulomb/pureconfig.scala b/pureconfig/src/main/scala/coulomb/pureconfig.scala index a1556bc3b..3dcab4476 100644 --- a/pureconfig/src/main/scala/coulomb/pureconfig.scala +++ b/pureconfig/src/main/scala/coulomb/pureconfig.scala @@ -24,15 +24,20 @@ import coulomb.rational.Rational import coulomb.parser.RuntimeUnitParser trait UnitPathMapper: - def path(expr: String): Either[String, String] + def path(unit: String): Either[String, String] + +trait PathUnitMapper: + def unit(path: String): String class PureconfigRuntime( cr: CoefficientRuntime, rup: RuntimeUnitParser, - upm: String => Either[String, String] + upm: String => Either[String, String], + pum: String => String ) extends CoefficientRuntime with RuntimeUnitParser - with UnitPathMapper: + with UnitPathMapper + with PathUnitMapper: def parse(expr: String): Either[String, RuntimeUnit] = rup.parse(expr) @@ -45,8 +50,10 @@ class PureconfigRuntime( ): Either[String, Rational] = cr.coefficientRational(uf, ut) - def path(expr: String): Either[String, String] = - upm(expr) + def path(unit: String): Either[String, String] = + upm(unit) + def unit(path: String): String = + pum(path) object PureconfigRuntime: import coulomb.conversion.runtimes.mapping.MappingCoefficientRuntime @@ -55,10 +62,15 @@ object PureconfigRuntime: inline def of[UTL <: Tuple]: PureconfigRuntime = val r = MappingCoefficientRuntime.of[UTL] val p = RuntimeUnitDslParser.of[UTL] - val u: (String => Either[String, String]) = (expr: String) => { - if (expr.isEmpty) Left(expr) - else if (expr.head == '@') Right(expr.tail) - else if (p.unames.contains(expr)) Right(p.unames(expr)) - else Left(expr) + val upm: (String => Either[String, String]) = (unit: String) => { + if (unit.isEmpty) Left(unit) + else if (unit.head == '@') Right(unit.tail) + else if (p.unames.contains(unit)) Right(p.unames(unit)) + else Left(unit) + } + val pum: (String => String) = (path: String) => { + if (path.isEmpty) throw new Exception("empty path string") + else if (p.unamesinv.contains(path)) p.unamesinv(path) + else s"@${path}" } - new PureconfigRuntime(r, p, u) + new PureconfigRuntime(r, p, upm, pum) From f5a4dee756a7dd542c5b482149c255d8001bf8a9 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Wed, 16 Aug 2023 15:49:23 -0700 Subject: [PATCH 110/141] remove operator addition example --- .../coulomb/conversion/runtime/scala.scala | 32 --------- runtime/src/main/scala/coulomb/ops/ops.scala | 31 -------- .../main/scala/coulomb/ops/runtime/add.scala | 71 ------------------- .../main/scala/coulomb/ops/runtime/all.scala | 20 ------ .../policy/overlay/runtime/policy.scala | 24 ------- .../main/scala/coulomb/runtime/runtime.scala | 4 -- 6 files changed, 182 deletions(-) delete mode 100644 runtime/src/main/scala/coulomb/conversion/runtime/scala.scala delete mode 100644 runtime/src/main/scala/coulomb/ops/ops.scala delete mode 100644 runtime/src/main/scala/coulomb/ops/runtime/add.scala delete mode 100644 runtime/src/main/scala/coulomb/ops/runtime/all.scala delete mode 100644 runtime/src/main/scala/coulomb/policy/overlay/runtime/policy.scala diff --git a/runtime/src/main/scala/coulomb/conversion/runtime/scala.scala b/runtime/src/main/scala/coulomb/conversion/runtime/scala.scala deleted file mode 100644 index 1c1646b65..000000000 --- a/runtime/src/main/scala/coulomb/conversion/runtime/scala.scala +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2022 Erik Erlandson - * - * 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 coulomb.conversion.runtime - -object scala: - import _root_.scala.Conversion - import coulomb.conversion.* - import coulomb.* - import coulomb.syntax.* - - given ctx_RuntimeQuantity_Conversion_1V[V] - : Conversion[RuntimeQuantity[V], RuntimeQuantity[V]] = - (q: RuntimeQuantity[V]) => q - - given ctx_RuntimeQuantity_Conversion_2V[VF, VT](using - vc: ValueConversion[VF, VT] - ): Conversion[RuntimeQuantity[VF], RuntimeQuantity[VT]] = - (q: RuntimeQuantity[VF]) => RuntimeQuantity(vc(q.value), q.unit) diff --git a/runtime/src/main/scala/coulomb/ops/ops.scala b/runtime/src/main/scala/coulomb/ops/ops.scala deleted file mode 100644 index c8f44ab33..000000000 --- a/runtime/src/main/scala/coulomb/ops/ops.scala +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2022 Erik Erlandson - * - * 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 coulomb.ops - -import scala.annotation.implicitNotFound - -import coulomb.* - -@implicitNotFound( - "Addition not defined in scope for RuntimeQuantity[${VL}] and RuntimeQuantity[${VR}]" -) -abstract class RuntimeAdd[VL, VR]: - type VO - val eval: ( - RuntimeQuantity[VL], - RuntimeQuantity[VR] - ) => Either[String, RuntimeQuantity[VO]] diff --git a/runtime/src/main/scala/coulomb/ops/runtime/add.scala b/runtime/src/main/scala/coulomb/ops/runtime/add.scala deleted file mode 100644 index da4838b56..000000000 --- a/runtime/src/main/scala/coulomb/ops/runtime/add.scala +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2022 Erik Erlandson - * - * 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 coulomb.ops.runtime - -object add: - import scala.util.NotGiven - import scala.Conversion - - import algebra.ring.{AdditiveSemigroup, MultiplicativeSemigroup} - - import coulomb.rational.Rational - import coulomb.{RuntimeQuantity, CoefficientRuntime} - import coulomb.syntax.withRuntimeUnit - import coulomb.ops.{RuntimeAdd, ValueResolution} - import coulomb.conversion.ValueConversion - - transparent inline given ctx_runtime_add_1V[VL, VR](using - eqv: VR =:= VL, - crt: CoefficientRuntime, - vcr: ValueConversion[Rational, VL], - alg: AdditiveSemigroup[VL], - mul: MultiplicativeSemigroup[VL] - ): RuntimeAdd[VL, VR] = - new infra.RTAddNC((ql: RuntimeQuantity[VL], qr: RuntimeQuantity[VR]) => - crt.coefficient[VL](qr.unit, ql.unit).map { coef => - alg.plus(ql.value, mul.times(coef, eqv(qr.value))) - .withRuntimeUnit(ql.unit) - } - ) - - transparent inline given ctx_runtime_add_2V[VL, VR](using - nev: NotGiven[VR =:= VL], - crt: CoefficientRuntime, - vres: ValueResolution[VL, VR], - icl: Conversion[RuntimeQuantity[VL], RuntimeQuantity[vres.VO]], - icr: Conversion[RuntimeQuantity[VR], RuntimeQuantity[vres.VO]], - vcr: ValueConversion[Rational, vres.VO], - alg: AdditiveSemigroup[vres.VO], - mul: MultiplicativeSemigroup[vres.VO] - ): RuntimeAdd[VL, VR] = - new infra.RTAddNC((ql: RuntimeQuantity[VL], qr: RuntimeQuantity[VR]) => - val qlo = icl(ql) - val qro = icr(qr) - crt.coefficient[vres.VO](qro.unit, qlo.unit).map { coef => - alg.plus(qlo.value, mul.times(coef, qro.value)) - .withRuntimeUnit(qlo.unit) - } - ) - - object infra: - class RTAddNC[VL, VR, VOp]( - val eval: ( - RuntimeQuantity[VL], - RuntimeQuantity[VR] - ) => Either[String, RuntimeQuantity[VOp]] - ) extends RuntimeAdd[VL, VR]: - type VO = VOp diff --git a/runtime/src/main/scala/coulomb/ops/runtime/all.scala b/runtime/src/main/scala/coulomb/ops/runtime/all.scala deleted file mode 100644 index 4cc486f60..000000000 --- a/runtime/src/main/scala/coulomb/ops/runtime/all.scala +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2022 Erik Erlandson - * - * 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 coulomb.ops.runtime - -object all: - export coulomb.ops.runtime.add.given diff --git a/runtime/src/main/scala/coulomb/policy/overlay/runtime/policy.scala b/runtime/src/main/scala/coulomb/policy/overlay/runtime/policy.scala deleted file mode 100644 index 3f93bf7fa..000000000 --- a/runtime/src/main/scala/coulomb/policy/overlay/runtime/policy.scala +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2022 Erik Erlandson - * - * 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 coulomb.policy.overlay.runtime - -object strict: - export coulomb.ops.runtime.all.given - -object standard: - export coulomb.ops.runtime.all.given - export coulomb.conversion.runtime.scala.given diff --git a/runtime/src/main/scala/coulomb/runtime/runtime.scala b/runtime/src/main/scala/coulomb/runtime/runtime.scala index 2d92e5f2e..9ed0e253b 100644 --- a/runtime/src/main/scala/coulomb/runtime/runtime.scala +++ b/runtime/src/main/scala/coulomb/runtime/runtime.scala @@ -121,10 +121,6 @@ object RuntimeQuantity: mul.times(coef, vc(ql.value)).withUnit[UR] } - transparent inline def +[VR](qr: RuntimeQuantity[VR])(using - add: RuntimeAdd[VL, VR] - ): Either[String, RuntimeQuantity[add.VO]] = add.eval(ql, qr) - trait CoefficientRuntime: def coefficientRational( uf: RuntimeUnit, From 807388d022a40d48e27f804d0656851170840a04 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Wed, 16 Aug 2023 15:49:49 -0700 Subject: [PATCH 111/141] add case for no value conversion --- pureconfig/src/main/scala/coulomb/io.scala | 51 ++++++++++++++++++++-- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/pureconfig/src/main/scala/coulomb/io.scala b/pureconfig/src/main/scala/coulomb/io.scala index c20e6f152..8bf5b7424 100644 --- a/pureconfig/src/main/scala/coulomb/io.scala +++ b/pureconfig/src/main/scala/coulomb/io.scala @@ -17,6 +17,7 @@ package coulomb.pureconfig.io import scala.util.{Try, Success, Failure} +import scala.util.NotGiven import scala.jdk.CollectionConverters.* @@ -178,6 +179,16 @@ object ruJSON: ConfigWriter.fromFunction[RuntimeUnit](u2cv) object quantity: + // https://github.com/lampepfl/dotty/discussions/18415 + case class GivenAll[T <: Tuple](t: T) + + given given_GivenAll_Tuple[H, T <: Tuple](using + h: H, + t: GivenAll[T] + ): GivenAll[H *: T] = + GivenAll(h *: t.t) + given given_GivenAll_Empty: GivenAll[EmptyTuple] = GivenAll(EmptyTuple) + given ctx_RuntimeQuantity_Reader[V](using ConfigReader[V], ConfigReader[RuntimeUnit] @@ -194,11 +205,13 @@ object quantity: (q.value, q.unit) } - inline given ctx_Quantity_Reader[V, U](using - crq: ConfigReader[RuntimeQuantity[V]], - crt: CoefficientRuntime, + // if we have a conversion from Rational to V, that is happy path + // since we can safely convert units (basically, fractional values). + inline given ctx_Quantity_Reader_VC[V, U](using vcr: ValueConversion[Rational, V], - mul: MultiplicativeSemigroup[V] + mul: MultiplicativeSemigroup[V], + crq: ConfigReader[RuntimeQuantity[V]], + crt: CoefficientRuntime ): ConfigReader[Quantity[V, U]] = ConfigReader[RuntimeQuantity[V]].emap { rq => crt.coefficient[V](rq.unit, RuntimeUnit.of[U]) match @@ -206,6 +219,36 @@ object quantity: case Left(e) => Left(CannotConvert(s"$rq", "Quantity", e)) } + // if there is no conversion from Rational to V in context, then + // we can still try to safely load, as long as U is identical + // (or equivalent) to the unit we are loading from + inline given ctx_Quantity_Reader_NoVC[V, U](using + nocv: NotGiven[ + GivenAll[(ValueConversion[Rational, V], MultiplicativeSemigroup[V])] + ], + crq: ConfigReader[RuntimeQuantity[V]], + crt: CoefficientRuntime + ): ConfigReader[Quantity[V, U]] = + ConfigReader[RuntimeQuantity[V]].emap { rq => + val ufrom = rq.unit + val uto = RuntimeUnit.of[U] + crt.coefficientRational(ufrom, uto) match + case Right(coef) => + if (coef == Rational.const1) + // units are same or equivalent (conversion coefficient is 1) + // so it is valid to load directly without applying conversion coefficient + Right(rq.value.withUnit[U]) + else + Left( + CannotConvert( + s"$rq", + "Quantity", + s"no safe conversion from $ufrom to $uto" + ) + ) + case Left(e) => Left(CannotConvert(s"$rq", "Quantity", e)) + } + inline given ctx_Quantity_Writer[V, U](using ConfigWriter[RuntimeQuantity[V]] ): ConfigWriter[Quantity[V, U]] = From e82cee4d98e3e0140d889005c8e3a9ed91c03e74 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Wed, 16 Aug 2023 15:56:50 -0700 Subject: [PATCH 112/141] factor to runtimeq --- pureconfig/src/main/scala/coulomb/io.scala | 27 +++++++++++-------- .../src/main/scala/coulomb/policy.scala | 2 ++ 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/pureconfig/src/main/scala/coulomb/io.scala b/pureconfig/src/main/scala/coulomb/io.scala index 8bf5b7424..e36636de8 100644 --- a/pureconfig/src/main/scala/coulomb/io.scala +++ b/pureconfig/src/main/scala/coulomb/io.scala @@ -178,17 +178,7 @@ object ruJSON: ) ConfigWriter.fromFunction[RuntimeUnit](u2cv) -object quantity: - // https://github.com/lampepfl/dotty/discussions/18415 - case class GivenAll[T <: Tuple](t: T) - - given given_GivenAll_Tuple[H, T <: Tuple](using - h: H, - t: GivenAll[T] - ): GivenAll[H *: T] = - GivenAll(h *: t.t) - given given_GivenAll_Empty: GivenAll[EmptyTuple] = GivenAll(EmptyTuple) - +object runtimeq: given ctx_RuntimeQuantity_Reader[V](using ConfigReader[V], ConfigReader[RuntimeUnit] @@ -205,6 +195,9 @@ object quantity: (q.value, q.unit) } +object quantity: + import givenall.{*, given} + // if we have a conversion from Rational to V, that is happy path // since we can safely convert units (basically, fractional values). inline given ctx_Quantity_Reader_VC[V, U](using @@ -255,3 +248,15 @@ object quantity: ConfigWriter[RuntimeQuantity[V]].contramap[Quantity[V, U]] { q => RuntimeQuantity(q.value, RuntimeUnit.of[U]) } + + private object givenall: + // https://github.com/lampepfl/dotty/discussions/18415 + case class GivenAll[T <: Tuple](t: T) + + given given_GivenAll_Tuple[H, T <: Tuple](using + h: H, + t: GivenAll[T] + ): GivenAll[H *: T] = + GivenAll(h *: t.t) + + given given_GivenAll_Empty: GivenAll[EmptyTuple] = GivenAll(EmptyTuple) diff --git a/pureconfig/src/main/scala/coulomb/policy.scala b/pureconfig/src/main/scala/coulomb/policy.scala index c72b45012..0d2006a02 100644 --- a/pureconfig/src/main/scala/coulomb/policy.scala +++ b/pureconfig/src/main/scala/coulomb/policy.scala @@ -19,9 +19,11 @@ package coulomb.pureconfig.policy object DSL: export coulomb.pureconfig.io.rational.given export coulomb.pureconfig.io.ruDSL.given + export coulomb.pureconfig.io.runtimeq.given export coulomb.pureconfig.io.quantity.given object JSON: export coulomb.pureconfig.io.rational.given export coulomb.pureconfig.io.ruJSON.given + export coulomb.pureconfig.io.runtimeq.given export coulomb.pureconfig.io.quantity.given From bd80c00ea4dba936107423710b34a049cd6a0e2c Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Wed, 16 Aug 2023 16:50:12 -0700 Subject: [PATCH 113/141] remove ops from unit tests --- .../test/scala/coulomb/runtimequantity.scala | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/runtime/src/test/scala/coulomb/runtimequantity.scala b/runtime/src/test/scala/coulomb/runtimequantity.scala index 6f1950cc1..d1b6dadd1 100644 --- a/runtime/src/test/scala/coulomb/runtimequantity.scala +++ b/runtime/src/test/scala/coulomb/runtimequantity.scala @@ -52,27 +52,3 @@ abstract class RuntimeQuantitySuite(using CoefficientRuntime) .toQuantity[Float, Second] .assertL } - - test("addition strict") { - import coulomb.policy.strict.given - import coulomb.policy.overlay.runtime.strict.given - - (1d.withRuntimeUnit[Meter] + 1d.withRuntimeUnit[Kilo * Meter]) - .assertR(RuntimeQuantity[Meter](1001d)) - - (1d.withRuntimeUnit[Second] + 1d.withRuntimeUnit[Kilo * Meter]).assertL - - assertCE(""" - (1.withRuntimeUnit[Meter] + 1d.withRuntimeUnit[Kilo * Meter]) - """) - } - - test("addition standard") { - import coulomb.policy.standard.given - import coulomb.policy.overlay.runtime.standard.given - - (1.withRuntimeUnit[Meter] + 1d.withRuntimeUnit[Kilo * Meter]) - .assertR(RuntimeQuantity[Meter](1001d)) - - (1.withRuntimeUnit[Second] + 1d.withRuntimeUnit[Kilo * Meter]).assertL - } From 335583d779edce420acf0b777c326b9faa7d265f Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 26 Aug 2023 11:20:02 -0700 Subject: [PATCH 114/141] resync ci.yml --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db80af1f5..94c18724e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,11 +80,11 @@ jobs: - name: Make target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/scala3') - run: mkdir -p runtime/.js/target spire/.js/target spire/.jvm/target benchmarks/target testkit/.native/target target all/target units/.jvm/target runtime/.native/target testkit/.js/target parser/.jvm/target unidocs/target .js/target parser/.js/target core/.native/target site/target pureconfig/.jvm/target spire/.native/target parser/.native/target core/.js/target units/.native/target runtime/.jvm/target core/.jvm/target refined/.native/target .jvm/target .native/target refined/.js/target refined/.jvm/target units/.js/target testkit/.jvm/target project/target + run: mkdir -p runtime/.js/target spire/.js/target spire/.jvm/target testkit/.native/target units/.jvm/target runtime/.native/target testkit/.js/target parser/.jvm/target unidocs/target parser/.js/target core/.native/target pureconfig/.jvm/target spire/.native/target parser/.native/target core/.js/target units/.native/target runtime/.jvm/target core/.jvm/target refined/.native/target refined/.js/target refined/.jvm/target units/.js/target testkit/.jvm/target project/target - name: Compress target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/scala3') - run: tar cf targets.tar runtime/.js/target spire/.js/target spire/.jvm/target benchmarks/target testkit/.native/target target all/target units/.jvm/target runtime/.native/target testkit/.js/target parser/.jvm/target unidocs/target .js/target parser/.js/target core/.native/target site/target pureconfig/.jvm/target spire/.native/target parser/.native/target core/.js/target units/.native/target runtime/.jvm/target core/.jvm/target refined/.native/target .jvm/target .native/target refined/.js/target refined/.jvm/target units/.js/target testkit/.jvm/target project/target + run: tar cf targets.tar runtime/.js/target spire/.js/target spire/.jvm/target testkit/.native/target units/.jvm/target runtime/.native/target testkit/.js/target parser/.jvm/target unidocs/target parser/.js/target core/.native/target pureconfig/.jvm/target spire/.native/target parser/.native/target core/.js/target units/.native/target runtime/.jvm/target core/.jvm/target refined/.native/target refined/.js/target refined/.jvm/target units/.js/target testkit/.jvm/target project/target - name: Upload target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/scala3') From 9fcfb05981618dd83298039360e70b84d23065d0 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 3 Sep 2023 17:55:04 -0700 Subject: [PATCH 115/141] stub out coulomb-pureconfig doc --- docs/coulomb-pureconfig.md | 4 ++++ docs/directory.conf | 1 + 2 files changed, 5 insertions(+) create mode 100644 docs/coulomb-pureconfig.md diff --git a/docs/coulomb-pureconfig.md b/docs/coulomb-pureconfig.md new file mode 100644 index 000000000..107533513 --- /dev/null +++ b/docs/coulomb-pureconfig.md @@ -0,0 +1,4 @@ +# coulomb-pureconfig + +The `coulomb-pureconfig` package defines `pureconfig` `ConfigReader` and `ConfigWriter` implicit context rules for `Quantity`, `RuntimeQuantity`, and `RuntimeUnit` objects. + diff --git a/docs/directory.conf b/docs/directory.conf index fd19860af..97df4f448 100644 --- a/docs/directory.conf +++ b/docs/directory.conf @@ -5,4 +5,5 @@ laika.navigationOrder = [ coulomb-units.md coulomb-spire.md coulomb-refined.md + coulomb-pureconfig.md ] From 9c28688248c964bee52b287ad9a2fa8b610484bd Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Wed, 6 Sep 2023 05:15:48 -0700 Subject: [PATCH 116/141] add note about publish to s01.oss.sonatype.org --- build.sbt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.sbt b/build.sbt index 001197b47..570a04612 100644 --- a/build.sbt +++ b/build.sbt @@ -6,6 +6,8 @@ ThisBuild / tlBaseVersion := "0.7" // publish settings +// artifacts now publish to s01.oss.sonatype.org, per: +// https://github.com/erikerlandson/coulomb/issues/500 ThisBuild / developers += tlGitHubDev("erikerlandson", "Erik Erlandson") ThisBuild / organization := "com.manyangled" ThisBuild / organizationName := "Erik Erlandson" From d1de6f692673aab6836b3310f77ef382a001f0a1 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Wed, 6 Sep 2023 05:17:02 -0700 Subject: [PATCH 117/141] quick start example --- docs/coulomb-pureconfig.md | 70 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/docs/coulomb-pureconfig.md b/docs/coulomb-pureconfig.md index 107533513..2bd3d74dd 100644 --- a/docs/coulomb-pureconfig.md +++ b/docs/coulomb-pureconfig.md @@ -2,3 +2,73 @@ The `coulomb-pureconfig` package defines `pureconfig` `ConfigReader` and `ConfigWriter` implicit context rules for `Quantity`, `RuntimeQuantity`, and `RuntimeUnit` objects. +## Quick Start + +Before you begin, it is recommended to first familiarize yourself with the +[coulomb introduction](README.md) +and +[coulomb concepts](concepts.md). + +### packages + +```scala +libraryDependencies += "com.manyangled" %% "coulomb-core" % "@VERSION@" +libraryDependencies += "com.manyangled" %% "coulomb-pureconfig" % "@VERSION@" +``` + +### import + +The following example imports basic `coulomb` and `coulomb-pureconfig` definitions + +```scala mdoc +// fundamental coulomb types and methods +import coulomb.* +import coulomb.syntax.* + +// algebraic definitions +import algebra.instances.all.given +import coulomb.ops.algebra.all.given + +// unit and value type policies for operations +import coulomb.policy.standard.given +import scala.language.implicitConversions + +// unit definitions +import coulomb.units.si.{*, given} +import coulomb.units.si.prefixes.{*, given} + +// pureconfig defs +import _root_.pureconfig.{*, given} + +// import coulomb-pureconfig defs +import coulomb.pureconfig.* +// use the DSL-based io definitions for RuntimeUnit objects +import coulomb.pureconfig.policy.DSL.given +``` + +### examples +Define a pureconfig runtime to enable io of coulomb objects. +You can list either package and object names, or type definitions. +When you provide a package or object name, as in the example below, +any type definitions inside that object will be included in the runtime. + +```scala mdoc +// define a pureconfig runtime with SI and SI prefix unit definitions +given given_pureconfig: PureconfigRuntime = PureconfigRuntime.of[ + "coulomb.units.si" *: + "coulomb.units.si.prefixes" *: + EmptyTuple +] +``` + +```scala mdoc +// a simple configuration with one quantity +val conf = ConfigSource.string("""{value: 3, unit: "kilometer / second^2"}""") + +// load the value, using a different (but compatible) unit type +val q = conf.load[Quantity[Float, Meter / (Second ^ 2)]] + +// attempting to load as an incompatible unit will fail +val f = conf.load[Quantity[Float, Meter / Second]] +``` + From c1fcadb8930466452ba8561e18117b06c22b97ea Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 10 Sep 2023 07:34:19 -0700 Subject: [PATCH 118/141] dealias to get consistent mappings --- .../coulomb/runtime/conversion/runtimes/mapping.scala | 5 ++++- .../src/main/scala/coulomb/runtime/infra/meta.scala | 11 +++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala b/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala index 921acb60e..2e6b32d37 100644 --- a/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala +++ b/runtime/src/main/scala/coulomb/runtime/conversion/runtimes/mapping.scala @@ -174,7 +174,10 @@ object meta: if (!msym.flags.is(Flags.Module)) report.errorAndAbort(s"$mname is not a module") val usyms = msym.typeMembers.filter(isUnitSym) - usyms.map(_.typeRef) + // dealiasing here because otherwise types exposed via export + // can confuse the mapping. + // see also: typeReprUT function in runtime/infra/meta.scala + usyms.map(_.typeRef.dealias) def isUnitSym(using Quotes)(sym: quotes.reflect.Symbol): Boolean = sym.typeRef match diff --git a/runtime/src/main/scala/coulomb/runtime/infra/meta.scala b/runtime/src/main/scala/coulomb/runtime/infra/meta.scala index 9ee790161..5b7269b99 100644 --- a/runtime/src/main/scala/coulomb/runtime/infra/meta.scala +++ b/runtime/src/main/scala/coulomb/runtime/infra/meta.scala @@ -128,10 +128,13 @@ object meta: tr: quotes.reflect.TypeRepr ): RuntimeUnit.UnitType = import quotes.reflect.* - // should add checking for types with type-args here - // possibly an explicit non dealiasing policy here would allow - // parameterized types to be handled via typedef aliases? - RuntimeUnit.UnitType(tr.typeSymbol.fullName) + // should I add checking for types with type-args here? + // de-aliasing here because otherwise type exposed via export + // can confuse the lookup. There might be a use for more complicated + // logic that handles cases where dealiasing is not what I want but + // until I see one I'm going to keep it simple. + // This pairs with dealias in moduleUnits function in mapping.scala + RuntimeUnit.UnitType(tr.dealias.typeSymbol.fullName) def fqTypeRepr(using Quotes)(path: String): quotes.reflect.TypeRepr = fqTypeRepr(path.split('.').toIndexedSeq) From 566852bcb26283dedcb407405577823314b7c28d Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 10 Sep 2023 07:35:00 -0700 Subject: [PATCH 119/141] case class Config example --- docs/coulomb-pureconfig.md | 61 +++++++++++++++++++++++++++++++------- 1 file changed, 50 insertions(+), 11 deletions(-) diff --git a/docs/coulomb-pureconfig.md b/docs/coulomb-pureconfig.md index 2bd3d74dd..ca18224cf 100644 --- a/docs/coulomb-pureconfig.md +++ b/docs/coulomb-pureconfig.md @@ -34,16 +34,15 @@ import coulomb.policy.standard.given import scala.language.implicitConversions // unit definitions -import coulomb.units.si.{*, given} import coulomb.units.si.prefixes.{*, given} +import coulomb.units.info.{*, given} +import coulomb.units.time.{*, given} // pureconfig defs import _root_.pureconfig.{*, given} -// import coulomb-pureconfig defs +// import basic coulomb-pureconfig defs import coulomb.pureconfig.* -// use the DSL-based io definitions for RuntimeUnit objects -import coulomb.pureconfig.policy.DSL.given ``` ### examples @@ -55,20 +54,60 @@ any type definitions inside that object will be included in the runtime. ```scala mdoc // define a pureconfig runtime with SI and SI prefix unit definitions given given_pureconfig: PureconfigRuntime = PureconfigRuntime.of[ - "coulomb.units.si" *: "coulomb.units.si.prefixes" *: + "coulomb.units.info" *: + "coulomb.units.time" *: EmptyTuple ] ``` ```scala mdoc -// a simple configuration with one quantity -val conf = ConfigSource.string("""{value: 3, unit: "kilometer / second^2"}""") +case class Config( + duration: Quantity[Double, Second], + storage: Quantity[Double, Giga * Byte], + bandwidth: Quantity[Float, (Mega * Bit) / Second] +) + +// use the DSL-based io definitions for RuntimeUnit objects +import coulomb.pureconfig.policy.DSL.given -// load the value, using a different (but compatible) unit type -val q = conf.load[Quantity[Float, Meter / (Second ^ 2)]] +// pureconfig case class io is not yet automatically defined in scala 3 +given given_ConfigLoader: ConfigReader[Config] = + ConfigReader.forProduct3("duration", "storage", "bandwidth") { + (d: Quantity[Double, Second], + s: Quantity[Double, Giga * Byte], + b: Quantity[Float, (Mega * Bit) / Second]) => + Config(d, s, b) + } +``` -// attempting to load as an incompatible unit will fail -val f = conf.load[Quantity[Float, Meter / Second]] +```scala mdoc +// define a configuration source +// this source uses units that are different than the Config type +// definition, but they are convertable +val source = ConfigSource.string(""" +{ + duration: {value: 10, unit: minute}, + storage: {value: 100, unit: megabyte}, + bandwidth: {value: 200, unit: "gigabyte / second"} +} +""") + +// this load will succeed, with automatic unit conversions +val conf = source.load[Config] +``` + +```scala mdoc +// this config has the wrong unit for bandwidth +val bad = ConfigSource.string(""" +{ + duration: {value: 10, unit: minute}, + storage: {value: 100, unit: megabyte}, + bandwidth: {value: 200, unit: "gigabyte"} +} +""") + +// this load will fail because bandwidth units are incompatible +val fail = bad.load[Config] ``` From 9d09c65be8c51fa6a3b07df159b4f0e5ccef74cc Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 10 Sep 2023 13:42:04 -0700 Subject: [PATCH 120/141] improve definition of ConfigReader for case class --- docs/coulomb-pureconfig.md | 44 ++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/docs/coulomb-pureconfig.md b/docs/coulomb-pureconfig.md index ca18224cf..74d3f0695 100644 --- a/docs/coulomb-pureconfig.md +++ b/docs/coulomb-pureconfig.md @@ -12,8 +12,16 @@ and ### packages ```scala -libraryDependencies += "com.manyangled" %% "coulomb-core" % "@VERSION@" +// coulomb pureconfig integrations libraryDependencies += "com.manyangled" %% "coulomb-pureconfig" % "@VERSION@" + +// dependencies +libraryDependencies += "com.manyangled" %% "coulomb-core" % "@VERSION@" +libraryDependencies += "com.manyangled" %% "coulomb-runtime" % "@VERSION@" +libraryDependencies += "com.manyangled" %% "coulomb-parser" % "@VERSION@" + +// coulomb predefined units +libraryDependencies += "com.manyangled" %% "coulomb-units" % "@VERSION@" ``` ### import @@ -49,7 +57,7 @@ import coulomb.pureconfig.* Define a pureconfig runtime to enable io of coulomb objects. You can list either package and object names, or type definitions. When you provide a package or object name, as in the example below, -any type definitions inside that object will be included in the runtime. +any unit type definitions inside that object will be included in the runtime. ```scala mdoc // define a pureconfig runtime with SI and SI prefix unit definitions @@ -61,18 +69,29 @@ given given_pureconfig: PureconfigRuntime = PureconfigRuntime.of[ ] ``` +For our example, we define a simple configuration class, +using values with units. + ```scala mdoc case class Config( duration: Quantity[Double, Second], storage: Quantity[Double, Giga * Byte], bandwidth: Quantity[Float, (Mega * Bit) / Second] ) +``` -// use the DSL-based io definitions for RuntimeUnit objects -import coulomb.pureconfig.policy.DSL.given +We will also define a ConfigReader for our config class, +because pureconfig in scala 3 does not currently support automatic +derivation of ConfigReader for case classes. -// pureconfig case class io is not yet automatically defined in scala 3 -given given_ConfigLoader: ConfigReader[Config] = +```scala mdoc +// defined with 'using' context so that this function defers +// resolution and can operate with multiple pureconfig io policies +given given_ConfigLoader(using + ConfigReader[Quantity[Double, Second]], + ConfigReader[Quantity[Double, Giga * Byte]], + ConfigReader[Quantity[Float, (Mega * Bit) / Second]] +): ConfigReader[Config] = ConfigReader.forProduct3("duration", "storage", "bandwidth") { (d: Quantity[Double, Second], s: Quantity[Double, Giga * Byte], @@ -81,7 +100,16 @@ given given_ConfigLoader: ConfigReader[Config] = } ``` -```scala mdoc +In this example, we will import the DSL-based derivations for +RuntimeUnit objects, and demonstrate that these rules will +automatically convert compatible units, and load successfully. + +If a configuration value has _incompatible_ units, +the load will fail with a corresponding error. +```scala mdoc:nest +// use the DSL-based io definitions for RuntimeUnit objects +import coulomb.pureconfig.policy.DSL.given + // define a configuration source // this source uses units that are different than the Config type // definition, but they are convertable @@ -95,9 +123,7 @@ val source = ConfigSource.string(""" // this load will succeed, with automatic unit conversions val conf = source.load[Config] -``` -```scala mdoc // this config has the wrong unit for bandwidth val bad = ConfigSource.string(""" { From fbd115620831db15bd976544b6293ef8ea74660a Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 10 Sep 2023 14:58:48 -0700 Subject: [PATCH 121/141] io policy section --- docs/coulomb-pureconfig.md | 78 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 3 deletions(-) diff --git a/docs/coulomb-pureconfig.md b/docs/coulomb-pureconfig.md index 74d3f0695..a589ee148 100644 --- a/docs/coulomb-pureconfig.md +++ b/docs/coulomb-pureconfig.md @@ -104,9 +104,7 @@ In this example, we will import the DSL-based derivations for RuntimeUnit objects, and demonstrate that these rules will automatically convert compatible units, and load successfully. -If a configuration value has _incompatible_ units, -the load will fail with a corresponding error. -```scala mdoc:nest +```scala mdoc // use the DSL-based io definitions for RuntimeUnit objects import coulomb.pureconfig.policy.DSL.given @@ -123,7 +121,12 @@ val source = ConfigSource.string(""" // this load will succeed, with automatic unit conversions val conf = source.load[Config] +``` +If a configuration value has _incompatible_ units, +the load will fail with a corresponding error. + +```scala mdoc // this config has the wrong unit for bandwidth val bad = ConfigSource.string(""" { @@ -137,3 +140,72 @@ val bad = ConfigSource.string(""" val fail = bad.load[Config] ``` +## IO Policies + +The `coulomb-pureconfig` integrations currently support two options for I/O "policies" +which differ primarily in how one represents unit information. +In the quick-start example above, the DSL-based policy was demonstrated. + +The second option is a JSON-based unit representation. +Here, the units are defined using a JSON structured unit expression. +This representation is more verbose, +but it is more amenable to explicitly structured expressions. + +```scala mdoc:reset:invisible +// if mdoc had push/pop, I would not have to copy all this +import coulomb.* +import coulomb.syntax.* +import algebra.instances.all.given +import coulomb.ops.algebra.all.given +import coulomb.policy.standard.given +import scala.language.implicitConversions +import coulomb.units.si.prefixes.{*, given} +import coulomb.units.info.{*, given} +import coulomb.units.time.{*, given} +import _root_.pureconfig.{*, given} +import coulomb.pureconfig.* + +given given_pureconfig: PureconfigRuntime = PureconfigRuntime.of[ + "coulomb.units.si.prefixes" *: + "coulomb.units.info" *: + "coulomb.units.time" *: + EmptyTuple +] + +case class Config( + duration: Quantity[Double, Second], + storage: Quantity[Double, Giga * Byte], + bandwidth: Quantity[Float, (Mega * Bit) / Second] +) + +given given_ConfigLoader(using + ConfigReader[Quantity[Double, Second]], + ConfigReader[Quantity[Double, Giga * Byte]], + ConfigReader[Quantity[Float, (Mega * Bit) / Second]] +): ConfigReader[Config] = + ConfigReader.forProduct3("duration", "storage", "bandwidth") { + (d: Quantity[Double, Second], + s: Quantity[Double, Giga * Byte], + b: Quantity[Float, (Mega * Bit) / Second]) => + Config(d, s, b) + } +``` + +```scala mdoc +// use the JSON-based io definitions for RuntimeUnit objects +import coulomb.pureconfig.policy.JSON.given + +// define a configuration source +// this source uses units that are different than the Config type +// definition, but they are convertable +val source = ConfigSource.string(""" +{ + duration: {value: 10, unit: minute}, + storage: {value: 100, unit: {lhs: mega, op: "*", rhs: byte}}, + bandwidth: {value: 200, unit: {lhs: {lhs: giga, op: "*", rhs: byte}, op: "/", rhs: second}} +} +""") + +// this load will succeed, with automatic unit conversions +val conf = source.load[Config] +``` From e3965e085a549ca7de0f60e986a2cfb50a6883fa Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Mon, 11 Sep 2023 18:23:37 -0700 Subject: [PATCH 122/141] integer i/o section --- docs/coulomb-pureconfig.md | 43 +++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/docs/coulomb-pureconfig.md b/docs/coulomb-pureconfig.md index a589ee148..715bf814d 100644 --- a/docs/coulomb-pureconfig.md +++ b/docs/coulomb-pureconfig.md @@ -140,6 +140,45 @@ val bad = ConfigSource.string(""" val fail = bad.load[Config] ``` +## Integer Values + +In coulomb, conversion operations on integer values are considered to be +[truncating][truncating conversions]. +They may lose precision due to integer truncation. +Truncating conversions are generally explicit only, +because this loss of precision is numerically unsafe. + +In pureconfig I/O, however, there is no way to explicitly invoke a truncating conversion. +To mitigate this difficulty, the `coulomb-pureconfig` integrations will load without error +if the conversion factor is exactly 1. + +@:callout(info) +The safest way to ensure unit conversions will always succeed is to use fractional value types +such as Float or Double. +If desired, +[coulomb-spire](coulomb-spire.md) +provides integrations for fractional value types of higher precision. +@:@ + +```scala mdoc +// source for a quantity value +val qsrc = ConfigSource.string(""" +{ + value: 3 + unit: megabyte +} +""") + +// loading integer value types will succeed when type matches the config +qsrc.load[Quantity[Int, Mega * Byte]] + +// it will also succeed whenever the conversion coefficient is exactly 1. +qsrc.load[Quantity[Long, Byte * Mega]] + +// if the conversion is not exactly 1, load will fail +qsrc.load[Quantity[Int, Kilo * Byte]] +``` + ## IO Policies The `coulomb-pureconfig` integrations currently support two options for I/O "policies" @@ -195,9 +234,7 @@ given given_ConfigLoader(using // use the JSON-based io definitions for RuntimeUnit objects import coulomb.pureconfig.policy.JSON.given -// define a configuration source -// this source uses units that are different than the Config type -// definition, but they are convertable +// this configuration source represents units in structured JSON val source = ConfigSource.string(""" { duration: {value: 10, unit: minute}, From 018c1eabd0b2c7f78f077f3adcf1b5880ed5dc10 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Tue, 12 Sep 2023 17:18:27 -0700 Subject: [PATCH 123/141] stub coulomb-runtime docs --- docs/coulomb-runtime.md | 5 +++++ docs/directory.conf | 1 + 2 files changed, 6 insertions(+) create mode 100644 docs/coulomb-runtime.md diff --git a/docs/coulomb-runtime.md b/docs/coulomb-runtime.md new file mode 100644 index 000000000..a7723db9d --- /dev/null +++ b/docs/coulomb-runtime.md @@ -0,0 +1,5 @@ +# coulomb-runtime + +The `coulomb-runtime` package implements `RuntimeQuantity` and `RuntimeUnit`. +Its primary use case at the time of this documentation is to support runtime I/O, +for example the `coulomb-pureconfig` package. diff --git a/docs/directory.conf b/docs/directory.conf index 97df4f448..49355a16c 100644 --- a/docs/directory.conf +++ b/docs/directory.conf @@ -6,4 +6,5 @@ laika.navigationOrder = [ coulomb-spire.md coulomb-refined.md coulomb-pureconfig.md + coulomb-runtime.md ] From 45c5399b50c10112c48057f4201800e847e361da Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 16 Sep 2023 13:26:48 -0700 Subject: [PATCH 124/141] stub coulomb-parser docs --- docs/coulomb-parser.md | 5 +++++ docs/directory.conf | 1 + 2 files changed, 6 insertions(+) create mode 100644 docs/coulomb-parser.md diff --git a/docs/coulomb-parser.md b/docs/coulomb-parser.md new file mode 100644 index 000000000..c24c0d34a --- /dev/null +++ b/docs/coulomb-parser.md @@ -0,0 +1,5 @@ +# coulomb-parser + +The `coulomb-parser` package defines a unit expression parsing API and a reference unit expression DSL, +which is used by runtime I/O integrations such as +[coulomb-pureconfig](coulomb-pureconfig.md) diff --git a/docs/directory.conf b/docs/directory.conf index 49355a16c..de44ef719 100644 --- a/docs/directory.conf +++ b/docs/directory.conf @@ -7,4 +7,5 @@ laika.navigationOrder = [ coulomb-refined.md coulomb-pureconfig.md coulomb-runtime.md + coulomb-parser.md ] From dc6e5fce8fc5021f4d452bf107ea69f763dd2b85 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 16 Sep 2023 13:37:53 -0700 Subject: [PATCH 125/141] add note about pureconfig and jvm only --- docs/coulomb-pureconfig.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/coulomb-pureconfig.md b/docs/coulomb-pureconfig.md index 715bf814d..b65afef19 100644 --- a/docs/coulomb-pureconfig.md +++ b/docs/coulomb-pureconfig.md @@ -2,6 +2,13 @@ The `coulomb-pureconfig` package defines `pureconfig` `ConfigReader` and `ConfigWriter` implicit context rules for `Quantity`, `RuntimeQuantity`, and `RuntimeUnit` objects. +@:callout(info) +At this time pureconfig does not cross-compile to ScalaJS or ScalaNative, +and so `coulomb-pureconfig` also only builds for JVM. +This issue is tracked by pureconfig at +[#1307](https://github.com/pureconfig/pureconfig/issues/1307). +@:@ + ## Quick Start Before you begin, it is recommended to first familiarize yourself with the From 512f99e63ddaa3ffd4b0dfd0ff65d38439702314 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 16 Sep 2023 14:46:56 -0700 Subject: [PATCH 126/141] some parsing example code --- docs/coulomb-parser.md | 85 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/docs/coulomb-parser.md b/docs/coulomb-parser.md index c24c0d34a..4e521ace9 100644 --- a/docs/coulomb-parser.md +++ b/docs/coulomb-parser.md @@ -3,3 +3,88 @@ The `coulomb-parser` package defines a unit expression parsing API and a reference unit expression DSL, which is used by runtime I/O integrations such as [coulomb-pureconfig](coulomb-pureconfig.md) + +## Quick Start + +Before you begin, it is recommended to first familiarize yourself with the +[coulomb-runtime](coulomb-runtime.md) +documentation. + +### packages + +```scala +libraryDependencies += "com.manyangled" %% "coulomb-parser" % "@VERSION@" + +// dependencies +libraryDependencies += "com.manyangled" %% "coulomb-core" % "@VERSION@" +libraryDependencies += "com.manyangled" %% "coulomb-runtime" % "@VERSION@" + +// coulomb predefined units +libraryDependencies += "com.manyangled" %% "coulomb-units" % "@VERSION@" +``` + +### import + +```scala mdoc +// fundamental coulomb types and methods +import coulomb.* +import coulomb.syntax.* + +// algebraic definitions +import algebra.instances.all.given +import coulomb.ops.algebra.all.given + +// unit and value type policies for operations +import coulomb.policy.standard.given +import scala.language.implicitConversions + +// unit definitions +import coulomb.units.si.{*, given} +import coulomb.units.si.prefixes.{*, given} +import coulomb.units.info.{*, given} +import coulomb.units.time.{*, given} + +// parsing definitions +import coulomb.parser.RuntimeUnitParser +import coulomb.parser.standard.RuntimeUnitDslParser +``` + +### examples + +```scala mdoc +val dslparser: RuntimeUnitParser = RuntimeUnitDslParser.of[ + "coulomb.units.si" *: + "coulomb.units.si.prefixes" *: + "coulomb.units.info" *: + EmptyTuple +] +``` + +```scala mdoc +val u1 = dslparser.parse("meter") +u1.toString +``` + +```scala mdoc +dslparser.parse("nope") +``` + +```scala mdoc +dslparser.parse("kilometer").toString + +dslparser.parse("megabyte").toString +``` + +```scala mdoc +dslparser.parse("kilometer/second^2").toString +``` + +```scala mdoc +dslparser.parse("(1000 * meter) / (second ^ 2)").toString +``` + +```scala mdoc +dslparser.parse("@coulomb.units.si$.Second") +``` + + From a225e8bb3e295bd978cc2ec3dd286cc02e5e8699 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 17 Sep 2023 09:19:15 -0700 Subject: [PATCH 127/141] add laika conf to support @:api --- build.sbt | 14 ++++++++++++++ docs/coulomb-parser.md | 3 +++ 2 files changed, 17 insertions(+) diff --git a/build.sbt b/build.sbt index 570a04612..7f8fbfeb4 100644 --- a/build.sbt +++ b/build.sbt @@ -200,6 +200,8 @@ lazy val unidocs = project // https://typelevel.org/sbt-typelevel/site.html // sbt docs/tlSitePreview // http://localhost:4242 +import laika.ast.ExternalTarget +import laika.rewrite.link.{LinkConfig, ApiLinks, SourceLinks, TargetDefinition} lazy val docs = project .in(file("site")) .dependsOn( @@ -217,6 +219,18 @@ lazy val docs = project // at least until I can get a better handle on how to work with them Compile / scalacOptions ~= (_.filterNot { x => x.startsWith("-W") }) ) + .settings( + laikaConfig := LaikaConfig.defaults + .withConfigValue( + LinkConfig.empty + .addApiLinks( + // default will be coulomb api + ApiLinks(baseUri = + "https://www.javadoc.io/doc/com.manyangled/coulomb-docs_3/latest/" + ) + ) + ) + ) // https://github.com/sbt/sbt-jmh // sbt "benchmarks/Jmh/run .*Benchmark" diff --git a/docs/coulomb-parser.md b/docs/coulomb-parser.md index 4e521ace9..fe565d178 100644 --- a/docs/coulomb-parser.md +++ b/docs/coulomb-parser.md @@ -51,6 +51,9 @@ import coulomb.parser.standard.RuntimeUnitDslParser ### examples +test link: +@:api(coulomb.Quantity$) + ```scala mdoc val dslparser: RuntimeUnitParser = RuntimeUnitDslParser.of[ "coulomb.units.si" *: From 2804e1ec4d10a89ef33e6e0229192220ea1af3f5 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 23 Sep 2023 14:25:25 -0700 Subject: [PATCH 128/141] add external target for Quantity typedef --- build.sbt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/build.sbt b/build.sbt index 7f8fbfeb4..9f7791a59 100644 --- a/build.sbt +++ b/build.sbt @@ -229,6 +229,12 @@ lazy val docs = project "https://www.javadoc.io/doc/com.manyangled/coulomb-docs_3/latest/" ) ) + .addTargets( + // Target names need to be all lowercase. + // Note, this does not align with Laika docs. + // intended usage: [Quantity][quantitytypedef] + TargetDefinition("quantitytypedef", ExternalTarget("https://www.javadoc.io/doc/com.manyangled/coulomb-docs_3/latest/coulomb.html#Quantity[V,U]=V")) + ) ) ) From 0098d09b9f5fc63bcf064fa5b45fa772c31079fa Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 23 Sep 2023 14:37:49 -0700 Subject: [PATCH 129/141] fix formatting --- build.sbt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 9f7791a59..4e3293d55 100644 --- a/build.sbt +++ b/build.sbt @@ -233,7 +233,12 @@ lazy val docs = project // Target names need to be all lowercase. // Note, this does not align with Laika docs. // intended usage: [Quantity][quantitytypedef] - TargetDefinition("quantitytypedef", ExternalTarget("https://www.javadoc.io/doc/com.manyangled/coulomb-docs_3/latest/coulomb.html#Quantity[V,U]=V")) + TargetDefinition( + "quantitytypedef", + ExternalTarget( + "https://www.javadoc.io/doc/com.manyangled/coulomb-docs_3/latest/coulomb.html#Quantity[V,U]=V" + ) + ) ) ) ) From cdb5d995da7be457f25d5e88ac072ffa3ace4629 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 23 Sep 2023 15:38:20 -0700 Subject: [PATCH 130/141] documented example code for coulomb-parser --- docs/coulomb-parser.md | 44 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/docs/coulomb-parser.md b/docs/coulomb-parser.md index fe565d178..efb2b79a0 100644 --- a/docs/coulomb-parser.md +++ b/docs/coulomb-parser.md @@ -51,8 +51,22 @@ import coulomb.parser.standard.RuntimeUnitDslParser ### examples -test link: -@:api(coulomb.Quantity$) +The core API for `coulomb-parser` is +@:api(coulomb.parser.RuntimeUnitParser). +A programmer may define their own parsing implementations against this API. +This package defines a reference implementation named +@:api(coulomb.parser.standard.RuntimeUnitDslParser), +which implements a DSL for representing +@:api(coulomb.RuntimeUnit) +types. +The examples that follow illustrate the DSL syntax and semantics. + +A +@:api(coulomb.parser.standard.RuntimeUnitDslParser) +is defined by giving it a list of package or object names, +which contain unit type definitions. +The following declaration creates a DSL parser that can understand +unit definitions for SI units, SI prefixes and information units. ```scala mdoc val dslparser: RuntimeUnitParser = RuntimeUnitDslParser.of[ @@ -63,31 +77,53 @@ val dslparser: RuntimeUnitParser = RuntimeUnitDslParser.of[ ] ``` +Parsing can fail, and so the `parse` method returns an `Either` object. +In the following code, parsing a known unit results in a successful `Right` value. + +@:callout(info) +Most of the following examples will display `toString` values to improve readability. +@:@ + ```scala mdoc val u1 = dslparser.parse("meter") u1.toString ``` +A parsing failure, such as a unit name that the parser does not know about, +results in a `Left` value containing a parsing error message. + ```scala mdoc dslparser.parse("nope") ``` +The reference DSL will parse any unit name that is composed of +a prefix followed by a unit, into its correct product: + ```scala mdoc dslparser.parse("kilometer").toString dslparser.parse("megabyte").toString ``` +As with static unit types and runtime units, +DSL units can be inductively combined with the operators `*`, `/` and `^`: + ```scala mdoc dslparser.parse("kilometer/second^2").toString ``` +Numeric literals can also appear, and are equivalent to literal +types in static unit type expressions: + ```scala mdoc dslparser.parse("(1000 * meter) / (second ^ 2)").toString ``` +You can also directly specify a fully qualified unit type name, +by prepending with an `@` symbol. +Note that these fully qualified names may use `name$` instead of `name`, +as with `si$` in the following: + ```scala mdoc dslparser.parse("@coulomb.units.si$.Second") ``` - - From 56e4be91214976efbe187ecd791866a1af0eba16 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 23 Sep 2023 16:09:33 -0700 Subject: [PATCH 131/141] add laika pr ref --- build.sbt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 4e3293d55..4bf2d3153 100644 --- a/build.sbt +++ b/build.sbt @@ -232,8 +232,10 @@ lazy val docs = project .addTargets( // Target names need to be all lowercase. // Note, this does not align with Laika docs. - // intended usage: [Quantity][quantitytypedef] + // In future laika releases the names will be case insensitive, see: + // https://github.com/typelevel/Laika/pull/541 TargetDefinition( + // intended usage: [Quantity][quantitytypedef] "quantitytypedef", ExternalTarget( "https://www.javadoc.io/doc/com.manyangled/coulomb-docs_3/latest/coulomb.html#Quantity[V,U]=V" From f82ff676cc741657d1a17c8528e52e916d2a0376 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 23 Sep 2023 16:31:20 -0700 Subject: [PATCH 132/141] add explaination --- build.sbt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.sbt b/build.sbt index 4bf2d3153..542bad620 100644 --- a/build.sbt +++ b/build.sbt @@ -236,6 +236,8 @@ lazy val docs = project // https://github.com/typelevel/Laika/pull/541 TargetDefinition( // intended usage: [Quantity][quantitytypedef] + // Links to type defs do not work properly with laika '@:api(...)' constructs + // which is going to make a lot of coulomb references harder to do. "quantitytypedef", ExternalTarget( "https://www.javadoc.io/doc/com.manyangled/coulomb-docs_3/latest/coulomb.html#Quantity[V,U]=V" From c0fe7b84e7f13aeeafeb1badce5af963c7d1fc72 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 23 Sep 2023 16:41:46 -0700 Subject: [PATCH 133/141] add coulomb package prefix --- build.sbt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 542bad620..aa324ddea 100644 --- a/build.sbt +++ b/build.sbt @@ -226,7 +226,8 @@ lazy val docs = project .addApiLinks( // default will be coulomb api ApiLinks(baseUri = - "https://www.javadoc.io/doc/com.manyangled/coulomb-docs_3/latest/" + "https://www.javadoc.io/doc/com.manyangled/coulomb-docs_3/latest/", + packagePrefix = "coulomb" ) ) .addTargets( From c262a1c1582f910cbca158548ad9d3afa6ff4860 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 24 Sep 2023 07:26:56 -0700 Subject: [PATCH 134/141] format troll --- build.sbt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index aa324ddea..0f446742d 100644 --- a/build.sbt +++ b/build.sbt @@ -225,8 +225,9 @@ lazy val docs = project LinkConfig.empty .addApiLinks( // default will be coulomb api - ApiLinks(baseUri = - "https://www.javadoc.io/doc/com.manyangled/coulomb-docs_3/latest/", + ApiLinks( + baseUri = + "https://www.javadoc.io/doc/com.manyangled/coulomb-docs_3/latest/", packagePrefix = "coulomb" ) ) From 08cf0bac6702bad72428bb792faa45beda153baa Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 24 Sep 2023 08:00:34 -0700 Subject: [PATCH 135/141] add scala 3 api links --- build.sbt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 0f446742d..875f17fad 100644 --- a/build.sbt +++ b/build.sbt @@ -224,11 +224,14 @@ lazy val docs = project .withConfigValue( LinkConfig.empty .addApiLinks( - // default will be coulomb api ApiLinks( baseUri = "https://www.javadoc.io/doc/com.manyangled/coulomb-docs_3/latest/", packagePrefix = "coulomb" + ), + ApiLinks( + baseUri = "https://scala-lang.org/api/3.x/", + packagePrefix = "scala" ) ) .addTargets( From 0e61ade1fd1225a57c2821d281180427d55b57a7 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 24 Sep 2023 08:01:00 -0700 Subject: [PATCH 136/141] more coulomb-parser doc --- docs/coulomb-parser.md | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/docs/coulomb-parser.md b/docs/coulomb-parser.md index efb2b79a0..a0a6e2aab 100644 --- a/docs/coulomb-parser.md +++ b/docs/coulomb-parser.md @@ -77,20 +77,34 @@ val dslparser: RuntimeUnitParser = RuntimeUnitDslParser.of[ ] ``` -Parsing can fail, and so the `parse` method returns an `Either` object. -In the following code, parsing a known unit results in a successful `Right` value. +Parsing can fail, and so the `parse` method returns an @:api(scala.util.Either) object. +In the following code, parsing a known unit `meter` results in a successful @:api(scala.util.Right) value. + +This example illustrates that unit names parse into a corresponding fully qualified +unit type name, as would be used in static unit type expressions. +@:api(coulomb.parser.standard.RuntimeUnitDslParser) +maintains a mapping between static unit types and their names, +as defined by the `showFull` method, +such as `"meter"` <-> `coulomb.units.si$.Meter` @:callout(info) -Most of the following examples will display `toString` values to improve readability. +Scala 3 metaprogramming returns package objects with the `$` suffix, +for example `si$` instead of `si`. +This is mostly transparent to operations, unless you wish to refer +directly to fully qualified types using the `@` prefix in the DSL, +as illustrated in later examples. @:@ + ```scala mdoc val u1 = dslparser.parse("meter") + +// Most of the following examples will display with `toString` to improve readability. u1.toString ``` A parsing failure, such as a unit name that the parser does not know about, -results in a `Left` value containing a parsing error message. +results in a @:api(scala.util.Left) value containing a parsing error message. ```scala mdoc dslparser.parse("nope") @@ -121,9 +135,9 @@ dslparser.parse("(1000 * meter) / (second ^ 2)").toString You can also directly specify a fully qualified unit type name, by prepending with an `@` symbol. -Note that these fully qualified names may use `name$` instead of `name`, +Note that these fully qualified names may require `name$` instead of `name`, as with `si$` in the following: ```scala mdoc -dslparser.parse("@coulomb.units.si$.Second") +dslparser.parse("@coulomb.units.si$.Second").toString ``` From 497b126b83c6a6eba8ac5d33bcb49fa4424d47d9 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 24 Sep 2023 10:26:40 -0700 Subject: [PATCH 137/141] predefine doc page links --- build.sbt | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 875f17fad..ac5c00afe 100644 --- a/build.sbt +++ b/build.sbt @@ -200,7 +200,7 @@ lazy val unidocs = project // https://typelevel.org/sbt-typelevel/site.html // sbt docs/tlSitePreview // http://localhost:4242 -import laika.ast.ExternalTarget +import laika.ast.{ExternalTarget, InternalTarget, VirtualPath} import laika.rewrite.link.{LinkConfig, ApiLinks, SourceLinks, TargetDefinition} lazy val docs = project .in(file("site")) @@ -247,6 +247,48 @@ lazy val docs = project ExternalTarget( "https://www.javadoc.io/doc/com.manyangled/coulomb-docs_3/latest/coulomb.html#Quantity[V,U]=V" ) + ), + TargetDefinition( + "coulomb-core", + InternalTarget( + VirtualPath.parse("concepts.md") + ) + ), + TargetDefinition( + "coulomb-units", + InternalTarget( + VirtualPath.parse("coulomb-units.md") + ) + ), + TargetDefinition( + "coulomb-spire", + InternalTarget( + VirtualPath.parse("coulomb-spire.md") + ) + ), + TargetDefinition( + "coulomb-refined", + InternalTarget( + VirtualPath.parse("coulomb-refined.md") + ) + ), + TargetDefinition( + "coulomb-runtime", + InternalTarget( + VirtualPath.parse("coulomb-runtime.md") + ) + ), + TargetDefinition( + "coulomb-parser", + InternalTarget( + VirtualPath.parse("coulomb-parser.md") + ) + ), + TargetDefinition( + "coulomb-pureconfig", + InternalTarget( + VirtualPath.parse("coulomb-pureconfig.md") + ) ) ) ) From 590d12d316ef883d0fa415c7413c6f8ec90741cf Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sun, 24 Sep 2023 12:00:35 -0700 Subject: [PATCH 138/141] quick start for coulomb-runtime --- docs/coulomb-runtime.md | 127 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 125 insertions(+), 2 deletions(-) diff --git a/docs/coulomb-runtime.md b/docs/coulomb-runtime.md index a7723db9d..3c5a28e22 100644 --- a/docs/coulomb-runtime.md +++ b/docs/coulomb-runtime.md @@ -1,5 +1,128 @@ # coulomb-runtime -The `coulomb-runtime` package implements `RuntimeQuantity` and `RuntimeUnit`. +The [coulomb-runtime] package implements +@:api(coulomb.RuntimeQuantity) and @:api(coulomb.RuntimeUnit). Its primary use case at the time of this documentation is to support runtime I/O, -for example the `coulomb-pureconfig` package. +for example the [coulomb-pureconfig] package. + +## Quick Start + +### packages + +```scala +libraryDependencies += "com.manyangled" %% "coulomb-runtime" % "@VERSION@" + +// dependencies +libraryDependencies += "com.manyangled" %% "coulomb-core" % "@VERSION@" + +// coulomb predefined units +libraryDependencies += "com.manyangled" %% "coulomb-units" % "@VERSION@" +``` + +### import + +```scala mdoc +// fundamental coulomb types and methods +// these include RuntimeUnit and RuntimeQuantity +import coulomb.* +import coulomb.syntax.* + +// algebraic definitions +import algebra.instances.all.given +import coulomb.ops.algebra.all.given + +// unit and value type policies for operations +import coulomb.policy.standard.given +import scala.language.implicitConversions + +// unit definitions +import coulomb.units.si.{*, given} +import coulomb.units.si.prefixes.{*, given} +import coulomb.units.info.{*, given} +import coulomb.units.time.{*, given} + +// runtime definitions +import coulomb.conversion.runtimes.mapping.MappingCoefficientRuntime +``` + +### examples + +@:api(coulomb.RuntimeUnit) is the core data structure of the [coulomb-runtime] package. +It is a parallel runtime implementation of the standard static unit types and analysis +defined in [coulomb-core]. + +The `RuntimeUnit.of` method makes it easy to create RuntimeUnit values from static unit types. +Additionally, you can apply the standard unit type operators `*`, `/` and `^` to build up unit expressions. + +```scala mdoc +// create RuntimeUnit values from static unit types +val k = RuntimeUnit.of[Kilo] +val d = RuntimeUnit.of[Meter] +val t = RuntimeUnit.of[Second] + +// Build up unit expression from other expressions +val kps = (k * d) / t + +// values can be displayed with toString for readability +kps.toString +``` + +The `RuntimeUnit.of` method can be used with static unit types of arbitrary form. + +```scala mdoc +val kps2 = RuntimeUnit.of[Kilo * Meter / Second] +kps2.toString +``` + +A @:api(coulomb.RuntimeQuantity) is a value paired with a RuntimeUnit, +and is the runtime analog of @:api(coulomb.Quantity$). +The following example demonstrates some ways to create RuntimeQuantity objects. + +```scala mdoc +// a RuntimeQuantity is a value paired with a RuntimeUnit +val rq = RuntimeQuantity(1f, kps) + +// declare a RuntimeQuantity with a given RuntimeUnit +1f.withRuntimeUnit(kps).toString + +// an equivalent RuntimeQuantity based on a static unit type +1f.withRuntimeUnit[(Kilo * Meter) / Second].toString +``` + +It is also possible to convert from @:api(coulomb.RuntimeQuantity) to @:api(coulomb.Quantity). +This is accomplished using a @:api(coulomb.CoefficientRuntime) in context. + +As this example shows, you can list package names, which will import any unit types +into the CoefficientRuntime. + +@:callout(info) +The @:api(coulomb.CoefficientRuntime) bridges the gap between runtime unit expresions and +static unit types. +It is what allows loading unit aware configurations, for example in [coulomb-pureconfig]. +@:@ + +```scala mdoc +// declare a coefficient runtime that knows about SI units and prefixes +given given_CRT: CoefficientRuntime = MappingCoefficientRuntime.of[ + "coulomb.units.si" *: + "coulomb.units.si.prefixes" *: + EmptyTuple +] +``` + +With a @:api(coulomb.CoefficientRuntime), we can convert from runtime quantities to +static typed quantities. +We cannot know at compile time if these conversions will succeed, +so these operations return an @:api(scala.util.Either) value. + +```scala mdoc +// reconstruct the equivalent Quantity +rq.toQuantity[Float, Kilo * Meter / Second] + +// valid conversions of value types or unit types will also succeed +rq.toQuantity[Double, Meter / Second] + +// attempting to convert to incompatible units will fail +rq.toQuantity[Float, Second] +``` + From 8cf593b67a453fac79b036d62e77c20e9ed64a1d Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 30 Sep 2023 12:42:08 -0700 Subject: [PATCH 139/141] doc massaging --- build.sbt | 11 +++++++++++ docs/{concepts.md => coulomb-core.md} | 10 ++++++++-- docs/coulomb-pureconfig.md | 9 ++++++--- docs/develop.md | 2 +- docs/directory.conf | 5 ++--- 5 files changed, 28 insertions(+), 9 deletions(-) rename docs/{concepts.md => coulomb-core.md} (99%) diff --git a/build.sbt b/build.sbt index ac5c00afe..5d1909011 100644 --- a/build.sbt +++ b/build.sbt @@ -232,6 +232,11 @@ lazy val docs = project ApiLinks( baseUri = "https://scala-lang.org/api/3.x/", packagePrefix = "scala" + ), + ApiLinks( + baseUri = + "https://javadoc.io/doc/com.github.pureconfig/pureconfig-core_3/latest/", + packagePrefix = "pureconfig" ) ) .addTargets( @@ -248,6 +253,12 @@ lazy val docs = project "https://www.javadoc.io/doc/com.manyangled/coulomb-docs_3/latest/coulomb.html#Quantity[V,U]=V" ) ), + TargetDefinition( + "coulomb-introduction", + InternalTarget( + VirtualPath.parse("README.md") + ) + ), TargetDefinition( "coulomb-core", InternalTarget( diff --git a/docs/concepts.md b/docs/coulomb-core.md similarity index 99% rename from docs/concepts.md rename to docs/coulomb-core.md index 00b0ab52c..8079252c4 100644 --- a/docs/concepts.md +++ b/docs/coulomb-core.md @@ -1,6 +1,12 @@ -# coulomb Concepts +# coulomb-core -```scala mdoc:invisible +This page describes the fundamental `coulomb` concepts, implemented in `coulomb-core`. + +## Quick Start + +### import + +```scala mdoc // fundamental coulomb types and methods import coulomb.* import coulomb.syntax.* diff --git a/docs/coulomb-pureconfig.md b/docs/coulomb-pureconfig.md index b65afef19..760ad29d1 100644 --- a/docs/coulomb-pureconfig.md +++ b/docs/coulomb-pureconfig.md @@ -1,6 +1,9 @@ # coulomb-pureconfig -The `coulomb-pureconfig` package defines `pureconfig` `ConfigReader` and `ConfigWriter` implicit context rules for `Quantity`, `RuntimeQuantity`, and `RuntimeUnit` objects. +The `coulomb-pureconfig` package defines `pureconfig` +@:api(pureconfig.ConfigReader) and @:api(pureconfig.ConfigWriter) +implicit context rules for +@:api(coulomb.Quantity$), @:api(coulomb.RuntimeQuantity), and @:api(coulomb.RuntimeUnit) objects. @:callout(info) At this time pureconfig does not cross-compile to ScalaJS or ScalaNative, @@ -12,9 +15,9 @@ This issue is tracked by pureconfig at ## Quick Start Before you begin, it is recommended to first familiarize yourself with the -[coulomb introduction](README.md) +[coulomb introduction][coulomb-introduction] and -[coulomb concepts](concepts.md). +[coulomb-core]. ### packages diff --git a/docs/develop.md b/docs/develop.md index 2f6d4e0f2..d12080f87 100644 --- a/docs/develop.md +++ b/docs/develop.md @@ -1,4 +1,4 @@ -# coulomb Development +# coulomb development ## sbt-typelevel diff --git a/docs/directory.conf b/docs/directory.conf index de44ef719..06c913a84 100644 --- a/docs/directory.conf +++ b/docs/directory.conf @@ -1,11 +1,10 @@ laika.navigationOrder = [ - README.md - concepts.md - develop.md + coulomb-core.md coulomb-units.md coulomb-spire.md coulomb-refined.md coulomb-pureconfig.md coulomb-runtime.md coulomb-parser.md + develop.md ] From 93e342840391da39ba4905e086e44152480ed095 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 30 Sep 2023 13:15:17 -0700 Subject: [PATCH 140/141] clean up link definitions --- build.sbt | 2 +- docs/README.md | 11 +++++++---- docs/coulomb-refined.md | 2 +- docs/coulomb-spire.md | 4 ++-- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/build.sbt b/build.sbt index 5d1909011..79841d9bd 100644 --- a/build.sbt +++ b/build.sbt @@ -262,7 +262,7 @@ lazy val docs = project TargetDefinition( "coulomb-core", InternalTarget( - VirtualPath.parse("concepts.md") + VirtualPath.parse("coulomb-core.md") ) ), TargetDefinition( diff --git a/docs/README.md b/docs/README.md index 70c0f5867..7957f3b95 100644 --- a/docs/README.md +++ b/docs/README.md @@ -71,10 +71,13 @@ val fail = time + dist | name | description | | ---: | :--- | -| `coulomb-core` | Provides core `coulomb` logic. Defines policies for `Int`, `Long`, `Float`, `Double`. | -| `coulomb-units` | Defines common units, including SI, MKSA, Accepted, time, temperature, and US traditional | -| `coulomb-spire` | Defines policies for working with Spire and Scala numeric types | - +| [coulomb-core] | Provides core `coulomb` logic. Defines policies for `Int`, `Long`, `Float`, `Double`. | +| [coulomb-units] | Defines common units, including SI, MKSA, Accepted, time, temperature, and US traditional | +| [coulomb-spire] | Defines policies for working with Spire and Scala numeric types | +| [coulomb-refined] | Unit analysis with typelevel [refined](https://github.com/fthomas/refined#refined-simple-refinement-types-for-scala) awareness | +| [coulomb-pureconfig] | Configuration I/O with @:api(coulomb.Quantity$) values | +| [coulomb-runtime] | Runtime units and quantities | +| [coulomb-parser] | Parsing of expressions into runtime units | ## Resources diff --git a/docs/coulomb-refined.md b/docs/coulomb-refined.md index dcb6e59be..c301d95ea 100644 --- a/docs/coulomb-refined.md +++ b/docs/coulomb-refined.md @@ -139,7 +139,7 @@ plus(x: Refined[V, P], y: Refined[V, P]): Refined[V, P] = Because the refined algebraic policy is an overlay, you can use it with your choice of base policies, for example with -[core policies](concepts.md#coulomb-policies) +[core policies](coulomb-core.md#coulomb-policies) or [spire policies](coulomb-spire.md#policies). @:@ diff --git a/docs/coulomb-spire.md b/docs/coulomb-spire.md index d23135d2a..2db23e890 100644 --- a/docs/coulomb-spire.md +++ b/docs/coulomb-spire.md @@ -130,5 +130,5 @@ and so `Int` promotes to `Real`, etc. The definition of promotions for `coulomb-spire` can be browsed [here](https://www.javadoc.io/doc/com.manyangled/coulomb-docs_3/latest/coulomb/ops/resolution/spire$.html) -[value-resolution-concepts]: concepts.md#value-promotion-and-resolution -[policy-concepts]: concepts.md#coulomb-policies +[value-resolution-concepts]: coulomb-core.md#value-promotion-and-resolution +[policy-concepts]: coulomb-core.md#coulomb-policies From 3f6db6dccf79cca120684257ac1e21c6588f1d66 Mon Sep 17 00:00:00 2001 From: Erik Erlandson Date: Sat, 30 Sep 2023 13:21:46 -0700 Subject: [PATCH 141/141] modernize links --- docs/coulomb-parser.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/docs/coulomb-parser.md b/docs/coulomb-parser.md index a0a6e2aab..c4f757df5 100644 --- a/docs/coulomb-parser.md +++ b/docs/coulomb-parser.md @@ -1,14 +1,11 @@ # coulomb-parser The `coulomb-parser` package defines a unit expression parsing API and a reference unit expression DSL, -which is used by runtime I/O integrations such as -[coulomb-pureconfig](coulomb-pureconfig.md) +which is used by runtime I/O integrations such as [coulomb-pureconfig] ## Quick Start -Before you begin, it is recommended to first familiarize yourself with the -[coulomb-runtime](coulomb-runtime.md) -documentation. +Before you begin, it is recommended to first familiarize yourself with the [coulomb-runtime] documentation. ### packages