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
+ }
+}