From dff5e263593d5c3aa535e1ec0ce5daf00144130b Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Sat, 23 Jul 2022 19:03:49 -0400 Subject: [PATCH 1/4] Reintroduce the ir module and added Name and most of Path --- README.md | 8 ++ build.sc | 14 ++ morphir/ir/src/morphir/ir/Name.scala | 118 ++++++++++++++++ morphir/ir/src/morphir/ir/Path.scala | 76 ++++++++++ morphir/ir/test/src/morphir/ir/NameSpec.scala | 133 ++++++++++++++++++ .../src/morphir/testing/MorphirBaseSpec.scala | 8 ++ 6 files changed, 357 insertions(+) create mode 100644 morphir/ir/src/morphir/ir/Name.scala create mode 100644 morphir/ir/src/morphir/ir/Path.scala create mode 100644 morphir/ir/test/src/morphir/ir/NameSpec.scala create mode 100644 morphir/ir/test/src/morphir/testing/MorphirBaseSpec.scala diff --git a/README.md b/README.md index 646c46a32..7eb9f6459 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,14 @@ Code needs to be formatted according to `scalafmt` rules. To run `scalafmt` on a ./mill mill.scalalib.scalafmt.ScalafmtModule/reformatAll __.sources ``` +or the much shorter: + +```bash +./mill reformatAll __.sources +``` + + + or in watch mode to reformat changed files: ```bash diff --git a/build.sc b/build.sc index ad6f53522..0abf33a12 100644 --- a/build.sc +++ b/build.sc @@ -35,6 +35,13 @@ object morphir extends Module { object test extends Tests with MorphirTestModule {} } + object ir extends mill.Cross[IrModule](ScalaVersions.all: _*) + + class IrModule(val crossScalaVersion: String) extends MorphirCrossScalaModule with MorphirPublishModule { + def ivyDeps = Agg(com.lihaoyi.sourcecode, dev.zio.zio, dev.zio.`zio-prelude`) + object test extends Tests with MorphirTestModule {} + } + object knowledge extends mill.Cross[KnowledgeModule](ScalaVersions.all: _*) {} class KnowledgeModule(val crossScalaVersion: String) extends MorphirCrossScalaModule { def ivyDeps = Agg(com.lihaoyi.sourcecode, dev.zio.`zio-streams`) @@ -78,3 +85,10 @@ object morphir extends Module { } } } + +import mill.eval.{Evaluator, EvaluatorPaths} +// With this we can now just do ./mill reformatAll __.sources +// instead of ./mill -w mill.scalalib.scalafmt.ScalafmtModule/reformatAll __.sources +def reformatAll(evaluator: Evaluator, sources: mill.main.Tasks[Seq[PathRef]]) = T.command { + ScalafmtModule.reformatAll(sources)() +} diff --git a/morphir/ir/src/morphir/ir/Name.scala b/morphir/ir/src/morphir/ir/Name.scala new file mode 100644 index 000000000..344926be2 --- /dev/null +++ b/morphir/ir/src/morphir/ir/Name.scala @@ -0,0 +1,118 @@ +package morphir.ir + +import zio.Chunk + +import scala.annotation.tailrec + +final case class Name private (toList: List[String]) extends AnyVal { self => + def :+(that: String): Name = Name(self.toList :+ that) + def +:(that: String): Name = Name(that +: self.toList) + def ++(that: Name): Name = Name(self.toList ++ that.toList) + def /(that: Name): Path = Path(Chunk(self, that)) + + def humanize: List[String] = { + val words = toList + val join: List[String] => String = abbrev => abbrev.map(_.toUpperCase()).mkString("") + + @tailrec + def loop( + prefix: List[String], + abbrev: List[String], + suffix: List[String] + ): List[String] = + suffix match { + case Nil => + abbrev match { + case Nil => prefix + case _ => prefix ++ List(join(abbrev)) + } + case first :: rest => + if (first.length() == 1) + loop(prefix, abbrev ++ List(first), rest) + else + abbrev match { + case Nil => loop(prefix ++ List(first), List.empty, rest) + case _ => + loop(prefix ++ List(join(abbrev), first), List.empty, rest) + } + } + + loop(List.empty, List.empty, words.toList) + } + + /** + * Maps segments of the `Name`. + */ + def mapParts(f: String => String): Name = Name(self.toList.map(f)) + + def mkString(f: String => String)(sep: String): String = + toList.map(f).mkString(sep) + + def toUpperCase: String = mkString(part => part.toUpperCase)("") + + def toLowerCase: String = + mkString(part => part.toLowerCase)("") + + def toCamelCase: String = + toList match { + case Nil => "" + case head :: tail => + (head :: tail.map(_.capitalize)).mkString("") + } + + def toKebabCase: String = + humanize.mkString("-") + + def toSnakeCase: String = + humanize.mkString("_") + + def toTitleCase: String = + toList + .map(_.capitalize) + .mkString("") + + override def toString: String = toList.mkString("[", ",", "]") +} + +object Name { + + val empty: Name = Name(Nil) + + private def wrap(value: List[String]): Name = Name(value) + + def apply(first: String, rest: String*): Name = + fromIterable(first +: rest) + + private val pattern = """[a-zA-Z][a-z]*|[0-9]+""".r + + @inline def fromList(list: List[String]): Name = fromIterable(list) + def fromIterable(iterable: Iterable[String]): Name = + wrap(iterable.flatMap(str => pattern.findAllIn(str)).map(_.toLowerCase).toList) + + def fromString(str: String): Name = + Name(pattern.findAllIn(str).toList.map(_.toLowerCase())) + + /** + * Creates a new name from a chunk of strings without checking. + */ + private[morphir] def unsafeMake(value: List[String]): Name = Name(value) + private[morphir] def unsafeMake(exactSegments: String*): Name = Name(exactSegments.toList) + + def toList(name: Name): List[String] = name.toList + + @inline def toTitleCase(name: Name): String = name.toTitleCase + + @inline def toCamelCase(name: Name): String = name.toCamelCase + + @inline def toSnakeCase(name: Name): String = name.toSnakeCase + + @inline def toKebabCase(name: Name): String = name.toKebabCase + + @inline def toHumanWords(name: Name): List[String] = name.humanize + + object VariableName { + def unapply(name: Name): Option[String] = + Some(name.toCamelCase) + } + +} diff --git a/morphir/ir/src/morphir/ir/Path.scala b/morphir/ir/src/morphir/ir/Path.scala new file mode 100644 index 000000000..fd8d58991 --- /dev/null +++ b/morphir/ir/src/morphir/ir/Path.scala @@ -0,0 +1,76 @@ +package morphir.ir + +import zio.Chunk +//import morphir.ir.PackageModule.PackageAndModulePath + +import scala.annotation.tailrec + +//import Module.{ModuleName, ModulePath} + +final case class Path(segments: Chunk[Name]) { self => + + def ++(that: Path): Path = Path(segments ++ that.segments) + + /** Constructs a new path by combining this path with the given name. */ + def /(name: Name): Path = Path(segments ++ Chunk(name)) + + /** Constructs a new path by combining this path with the given path. */ + def /(that: Path): Path = Path(segments ++ that.segments) + // def %(other: Path): PackageAndModulePath = + // PackageAndModulePath(PackageName(self), ModulePath(other)) + + // def %(name: Name): ModuleName = ModuleName(self, name) + // def ::(name: Name): QName = QName(self, name) + + /** Indicates whether this path is empty. */ + def isEmpty: Boolean = segments.isEmpty + def zip(other: Path): (Path, Path) = (self, other) + + def toList: List[Name] = segments.toList + + def toString(f: Name => String, separator: String): String = + segments.map(f).mkString(separator) + + /** Checks if this path is a prefix of provided path */ + def isPrefixOf(path: Path): Boolean = Path.isPrefixOf(self, path) +} + +object Path { + val empty: Path = Path(Chunk.empty) + + def apply(first: String, rest: String*): Path = + wrap((first +: rest).map(Name.fromString).toList) + + def apply(first: Name, rest: Name*): Path = + wrap((first +: rest).toList) + + private def wrap(value: List[Name]): Path = Path(Chunk.fromIterable(value)) + + def fromString(str: String): Path = { + val separatorRegex = """[^\w\s]+""".r + wrap(separatorRegex.split(str).map(Name.fromString).toList) + } + + def toString(f: Name => String, separator: String, path: Path): String = + path.toString(f, separator) + + @inline def fromList(names: List[Name]): Path = wrap(names) + + @inline def toList(path: Path): List[Name] = path.segments.toList + + /** Checks if the first provided path is a prefix of the second path */ + @tailrec + def isPrefixOf(prefix: Path, path: Path): Boolean = (prefix.toList, path.toList) match { + case (Nil, _) => true + case (_, Nil) => false + case (prefixHead :: prefixTail, pathHead :: pathTail) => + if (prefixHead == pathHead) + isPrefixOf( + Path.fromList(prefixTail), + Path.fromList(pathTail) + ) + else false + } + + private[morphir] def unsafeMake(parts: Name*): Path = Path(Chunk.fromIterable(parts)) +} diff --git a/morphir/ir/test/src/morphir/ir/NameSpec.scala b/morphir/ir/test/src/morphir/ir/NameSpec.scala new file mode 100644 index 000000000..a9e08be03 --- /dev/null +++ b/morphir/ir/test/src/morphir/ir/NameSpec.scala @@ -0,0 +1,133 @@ +package morphir.ir + +import morphir.testing.MorphirBaseSpec +import zio.test._ + +object NameSpec extends MorphirBaseSpec { + def spec = suite("Name")( + suite("Create a Name from a string and check that:")( + suite("Name should be creatable from a single word that:")( + test("Starts with a capital letter") { + assertTrue(Name.fromString("Marco") == Name("marco")) + }, + test("Is all lowercase") { + assertTrue(Name.fromString("polo") == Name("polo")) + } + ), + suite("Name should be creatable from compound words that:")( + test("Are formed from a snake case word") { + assertTrue(Name.fromString("super_mario_world") == Name("super", "mario", "world")) + }, + test("Contain many kinds of word delimiters") { + assertTrue(Name.fromString("fooBar_baz 123") == Name("foo", "bar", "baz", "123")) + }, + test("Are formed from a camel-cased string") { + assertTrue(Name.fromString("valueInUSD") == Name("value", "in", "u", "s", "d")) + }, + test("Are formed from a title-cased string") { + assertTrue( + Name.fromString("ValueInUSD") == Name("value", "in", "u", "s", "d"), + Name.fromString("ValueInJPY") == Name("value", "in", "j", "p", "y") + ) + }, + test("Have a number in the middle") { + assertTrue(Name.fromString("Nintendo64VideoGameSystem") == Name("nintendo", "64", "video", "game", "system")) + }, + test("Are complete and utter nonsense") { + assertTrue(Name.fromString("_-%") == Name.empty) + } + ), + test("It splits the name as expected") { + // "fooBar","blahBlah" => ["foo","bar","blah","blah"] + // "fooBar","blahBlah" => ["fooBar","blahBlah"] + assertTrue( + Name.fromString("fooBar").toList == List("foo", "bar") + ) + } + ), + suite("Name should be convertible to a title-case string:")( + test("When the name was originally constructed from a snake-case string") { + val sut = Name.fromString("snake_case_input") + assertTrue(Name.toTitleCase(sut) == "SnakeCaseInput") + }, + test( + "When the name was originally constructed from a camel-case string" + ) { + val sut = Name.fromString("camelCaseInput") + assertTrue(Name.toTitleCase(sut) == "CamelCaseInput") + } + ), + suite("Name should be convertible to a camel-case string:")( + test( + "When the name was originally constructed from a snake-case string" + ) { + val sut = Name.fromString("snake_case_input") + assertTrue(Name.toCamelCase(sut) == "snakeCaseInput") + }, + test( + "When the name was originally constructed from a camel-case string" + ) { + val sut = Name.fromString("camelCaseInput") + assertTrue(Name.toCamelCase(sut) == "camelCaseInput") + } + ), + suite("Name should be convertible to snake-case")( + test("When given a name constructed from a list of words") { + val input = Name.fromList(List("foo", "bar", "baz", "123")) + assertTrue(Name.toSnakeCase(input) == "foo_bar_baz_123") + }, + test("When the name has parts of an abbreviation") { + val name = Name.fromList(List("value", "in", "u", "s", "d")) + assertTrue(Name.toSnakeCase(name) == "value_in_USD") + } + ), + suite("Name should be convertible to kebab-case")( + test("When given a name constructed from a list of words") { + val input = Name.fromList(List("foo", "bar", "baz", "123")) + assertTrue(Name.toKebabCase(input) == "foo-bar-baz-123") + }, + test("When the name has parts of an abbreviation") { + val name = Name.fromList(List("value", "in", "u", "s", "d")) + assertTrue(Name.toKebabCase(name) == "value-in-USD") + } + ), + suite("Name toHumanWords should provide a list of words from a Name")( + test("When the name is from a camelCase string") { + val sut = Name.fromString("ValueInUSD") + assertTrue(Name.toHumanWords(sut) == List("value", "in", "USD")) + } + ), + suite("fromIterable")( + test("Splits provided names as expected") { + assertTrue(Name.fromIterable(List("fooBar", "fizzBuzz")) == Name("foo", "bar", "fizz", "buzz")) + } + ), + suite("unsafeMake")( + test("Creates the name as provided") { + assertTrue( + Name.unsafeMake("foo", "bar", "baz", "123").toList == List("foo", "bar", "baz", "123") + ) + } + ), + suite("Misc")( + test("Name.toString") { + assertTrue(Name.fromString("fooBar").toString == "[foo,bar]", Name.fromString("a").toString == "[a]") + } + ), + suite("VariableName")( + test("When calling VariableName.unapply") { + val sut = Name.fromString("InspectorGadget") + val variableName = Name.VariableName.unapply(sut) + assertTrue(variableName == Some("inspectorGadget")) + }, + test("When using as an extractor") { + val sut = Name.fromString("IronMan") + val actual = sut match { + case Name.VariableName(variableName) => variableName + case _ => "not a variable name" + } + assertTrue(actual == "ironMan") + } + ) + ) +} diff --git a/morphir/ir/test/src/morphir/testing/MorphirBaseSpec.scala b/morphir/ir/test/src/morphir/testing/MorphirBaseSpec.scala new file mode 100644 index 000000000..b54db8598 --- /dev/null +++ b/morphir/ir/test/src/morphir/testing/MorphirBaseSpec.scala @@ -0,0 +1,8 @@ +package morphir.testing + +import zio._ +import zio.test.{TestAspect, ZIOSpecDefault} + +abstract class MorphirBaseSpec extends ZIOSpecDefault { + override def aspects = Chunk(TestAspect.timeout(60.seconds)) +} From 1f532ce057cf874ff8077c7918532be4a49e10d7 Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Sat, 23 Jul 2022 19:04:59 -0400 Subject: [PATCH 2/4] Remove .ammonite folder --- .../src/ammonite/$file/project/deps.scala | 93 ------------- .../project/modules/dependencyCheck.scala | 78 ----------- .../$file/project/modules/shared.scala | 123 ------------------ .../ammonite/$file/project/publishing.scala | 99 -------------- 4 files changed, 393 deletions(-) delete mode 100644 .ammonite/scala-2.13.7/amm-2.5.4/project/deps/src/ammonite/$file/project/deps.scala delete mode 100644 .ammonite/scala-2.13.7/amm-2.5.4/project/modules/dependencyCheck/src/ammonite/$file/project/modules/dependencyCheck.scala delete mode 100644 .ammonite/scala-2.13.7/amm-2.5.4/project/modules/shared/src/ammonite/$file/project/modules/shared.scala delete mode 100644 .ammonite/scala-2.13.7/amm-2.5.4/project/publishing/src/ammonite/$file/project/publishing.scala diff --git a/.ammonite/scala-2.13.7/amm-2.5.4/project/deps/src/ammonite/$file/project/deps.scala b/.ammonite/scala-2.13.7/amm-2.5.4/project/deps/src/ammonite/$file/project/deps.scala deleted file mode 100644 index ae7e12c3a..000000000 --- a/.ammonite/scala-2.13.7/amm-2.5.4/project/deps/src/ammonite/$file/project/deps.scala +++ /dev/null @@ -1,93 +0,0 @@ - -package ammonite -package $file.project -import _root_.ammonite.interp.api.InterpBridge.{ - value => interp -} -import _root_.ammonite.interp.api.InterpBridge.value.{ - exit, - scalaVersion -} -import _root_.ammonite.interp.api.IvyConstructor.{ - ArtifactIdExt, - GroupIdExt -} -import _root_.ammonite.compiler.CompilerExtensions.{ - CompilerInterpAPIExtensions, - CompilerReplAPIExtensions -} -import _root_.ammonite.runtime.tools.{ - browse, - grep, - time, - tail -} -import _root_.ammonite.compiler.tools.{ - desugar, - source -} -import _root_.mainargs.{ - arg, - main -} -import _root_.ammonite.repl.tools.Util.{ - PathRead -} - - -object deps{ -/**/ /**/ -def $main() = { scala.Iterator[String]() } - override def toString = "deps" - /**/ -} diff --git a/.ammonite/scala-2.13.7/amm-2.5.4/project/modules/dependencyCheck/src/ammonite/$file/project/modules/dependencyCheck.scala b/.ammonite/scala-2.13.7/amm-2.5.4/project/modules/dependencyCheck/src/ammonite/$file/project/modules/dependencyCheck.scala deleted file mode 100644 index 2ca0b7bdb..000000000 --- a/.ammonite/scala-2.13.7/amm-2.5.4/project/modules/dependencyCheck/src/ammonite/$file/project/modules/dependencyCheck.scala +++ /dev/null @@ -1,78 +0,0 @@ - -package ammonite -package $file.project.modules -import _root_.ammonite.interp.api.InterpBridge.{ - value => interp -} -import _root_.ammonite.interp.api.InterpBridge.value.{ - exit, - scalaVersion -} -import _root_.ammonite.interp.api.IvyConstructor.{ - ArtifactIdExt, - GroupIdExt -} -import _root_.ammonite.compiler.CompilerExtensions.{ - CompilerInterpAPIExtensions, - CompilerReplAPIExtensions -} -import _root_.ammonite.runtime.tools.{ - browse, - grep, - time, - tail -} -import _root_.ammonite.compiler.tools.{ - desugar, - source -} -import _root_.mainargs.{ - arg, - main -} -import _root_.ammonite.repl.tools.Util.{ - PathRead -} - - -object dependencyCheck{ -/**/ /**/ -def $main() = { scala.Iterator[String]() } - override def toString = "dependencyCheck" - /**/ -} diff --git a/.ammonite/scala-2.13.7/amm-2.5.4/project/modules/shared/src/ammonite/$file/project/modules/shared.scala b/.ammonite/scala-2.13.7/amm-2.5.4/project/modules/shared/src/ammonite/$file/project/modules/shared.scala deleted file mode 100644 index 38c3f5e62..000000000 --- a/.ammonite/scala-2.13.7/amm-2.5.4/project/modules/shared/src/ammonite/$file/project/modules/shared.scala +++ /dev/null @@ -1,123 +0,0 @@ - -package ammonite -package $file.project.modules -import _root_.ammonite.interp.api.InterpBridge.{ - value => interp -} -import _root_.ammonite.interp.api.InterpBridge.value.{ - exit, - scalaVersion -} -import _root_.ammonite.interp.api.IvyConstructor.{ - ArtifactIdExt, - GroupIdExt -} -import _root_.ammonite.compiler.CompilerExtensions.{ - CompilerInterpAPIExtensions, - CompilerReplAPIExtensions -} -import _root_.ammonite.runtime.tools.{ - browse, - grep, - time, - tail -} -import _root_.ammonite.compiler.tools.{ - desugar, - source -} -import _root_.mainargs.{ - arg, - main -} -import _root_.ammonite.repl.tools.Util.{ - PathRead -} -import ammonite.$file.project.{ - deps -} -import ammonite.$file.project.modules.{ - dependencyCheck -} - - -object shared{ -/**/ /**/ -def $main() = { scala.Iterator[String]() } - override def toString = "shared" - /**/ -} diff --git a/.ammonite/scala-2.13.7/amm-2.5.4/project/publishing/src/ammonite/$file/project/publishing.scala b/.ammonite/scala-2.13.7/amm-2.5.4/project/publishing/src/ammonite/$file/project/publishing.scala deleted file mode 100644 index 228b2328d..000000000 --- a/.ammonite/scala-2.13.7/amm-2.5.4/project/publishing/src/ammonite/$file/project/publishing.scala +++ /dev/null @@ -1,99 +0,0 @@ - -package ammonite -package $file.project -import _root_.ammonite.interp.api.InterpBridge.{ - value => interp -} -import _root_.ammonite.interp.api.InterpBridge.value.{ - exit, - scalaVersion -} -import _root_.ammonite.interp.api.IvyConstructor.{ - ArtifactIdExt, - GroupIdExt -} -import _root_.ammonite.compiler.CompilerExtensions.{ - CompilerInterpAPIExtensions, - CompilerReplAPIExtensions -} -import _root_.ammonite.runtime.tools.{ - browse, - grep, - time, - tail -} -import _root_.ammonite.compiler.tools.{ - desugar, - source -} -import _root_.mainargs.{ - arg, - main -} -import _root_.ammonite.repl.tools.Util.{ - PathRead -} - - -object publishing{ -/**/ /**/ -def $main() = { scala.Iterator[String]() } - override def toString = "publishing" - /**/ -} From f66c78927f5b3d063ec79a7b44ece6d9e87067bc Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Sat, 23 Jul 2022 19:05:43 -0400 Subject: [PATCH 3/4] Ignore the .ammonite folder --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 1b21b93be..974cfa9a6 100644 --- a/.gitignore +++ b/.gitignore @@ -420,7 +420,7 @@ project/plugins/project/ .lib/ *.class *.log - +.ammonite/ .metals/ metals.sbt .bloop/ From effd07bfc562eb44a496c26e588f680085f3f396 Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Sat, 23 Jul 2022 19:21:27 -0400 Subject: [PATCH 4/4] Add QName back --- morphir/ir/src/morphir/ir/Path.scala | 2 +- morphir/ir/src/morphir/ir/QName.scala | 28 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 morphir/ir/src/morphir/ir/QName.scala diff --git a/morphir/ir/src/morphir/ir/Path.scala b/morphir/ir/src/morphir/ir/Path.scala index fd8d58991..d2b089af8 100644 --- a/morphir/ir/src/morphir/ir/Path.scala +++ b/morphir/ir/src/morphir/ir/Path.scala @@ -20,7 +20,7 @@ final case class Path(segments: Chunk[Name]) { self => // PackageAndModulePath(PackageName(self), ModulePath(other)) // def %(name: Name): ModuleName = ModuleName(self, name) - // def ::(name: Name): QName = QName(self, name) + def ::(name: Name): QName = QName(self, name) /** Indicates whether this path is empty. */ def isEmpty: Boolean = segments.isEmpty diff --git a/morphir/ir/src/morphir/ir/QName.scala b/morphir/ir/src/morphir/ir/QName.scala new file mode 100644 index 000000000..64a8a4388 --- /dev/null +++ b/morphir/ir/src/morphir/ir/QName.scala @@ -0,0 +1,28 @@ +package morphir.ir + +final case class QName(modulePath: Path, localName: Name) { + @inline def toTuple: (Path, Name) = (modulePath, localName) + + override def toString: String = + modulePath.toString(Name.toTitleCase, ".") + ":" + localName.toCamelCase + +} + +object QName { + def toTuple(qName: QName): (Path, Name) = qName.toTuple + def fromTuple(tuple: (Path, Name)): QName = QName(tuple._1, tuple._2) + + def fromName(modulePath: Path, localName: Name): QName = QName(modulePath, localName) + + def getLocalName(qname: QName): Name = qname.localName + def getModulePath(qname: QName): Path = qname.modulePath + + def toString(qName: QName): String = qName.toString + + def fromString(str: String): Option[QName] = + str.split(":") match { + case Array(packageNameString, localNameString) => + Some(QName(Path.fromString(packageNameString), Name.fromString(localNameString))) + case _ => None + } +}