Skip to content

Commit

Permalink
refactor and support many new operations
Browse files Browse the repository at this point in the history
  • Loading branch information
kitlangton committed Mar 3, 2023
1 parent 2e1f76b commit 61d8670
Show file tree
Hide file tree
Showing 14 changed files with 730 additions and 289 deletions.
10 changes: 10 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,16 @@ lazy val zioQuill = (project in file("modules/neotype-zio-quill"))
)
.dependsOn(core)

lazy val zio = (project in file("modules/neotype-zio"))
.settings(
name := "neotype-zio",
sharedSettings,
libraryDependencies ++= Seq(
"dev.zio" %% "zio" % zioVersion
)
)
.dependsOn(core)

lazy val examples = (project in file("examples"))
.settings(
name := "neotype-examples",
Expand Down
1 change: 1 addition & 0 deletions examples/src/main/scala/neotype/examples/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package neotype.examples
import neotype.*

object Main extends App:
Email("[email protected]") // OK
FourSeasons("Spring") // OK
NonEmptyString("Good") // OK
FiveElements("REALLY LONG ELEMENT") // OK
10 changes: 9 additions & 1 deletion examples/src/main/scala/neotype/examples/Newtypes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,23 @@ package neotype.examples

import neotype.*

type NonEmptyString = NonEmptyString.Type
given NonEmptyString: Newtype[String] with
inline def validate(value: String): Boolean =
value.nonEmpty
value.reverse.nonEmpty

type Email = Email.Type
given Email: Newtype[String] with
inline def validate(value: String): Boolean =
value.contains("@") && value.contains(".")

type FourSeasons = FourSeasons.Type
given FourSeasons: Newtype[String] with
inline def validate(value: String): Boolean =
val seasons = Set("Spring", "Summer", "Autumn", "Winter")
seasons.contains(value)

type FiveElements = FiveElements.Type
given FiveElements: Newtype[String] with
inline def validate(value: String): Boolean =
value match
Expand Down
402 changes: 156 additions & 246 deletions modules/core/src/main/scala/neotype/Calc.scala

Large diffs are not rendered by default.

46 changes: 46 additions & 0 deletions modules/core/src/main/scala/neotype/CustomFromExpr.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package neotype

import scala.quoted.*

object CustomFromExpr:
given [A]: FromExpr[Set[A]] with
def unapply(x: Expr[Set[A]])(using Quotes) =
import quotes.reflect.*
val aType = x.asTerm.tpe.widen.typeArgs.head.asType
given FromExpr[A] = fromExprForType(aType).asInstanceOf[FromExpr[A]]
given Type[A] = aType.asInstanceOf[Type[A]]
x match
case '{ Set[A](${ Varargs(Exprs(elems)) }*) } => Some(elems.toSet)
case '{ Set.empty[A] } => Some(Set.empty[A])
case '{ scala.collection.immutable.Set[A](${ Varargs(Exprs(elems)) }*) } => Some(elems.toSet)
case '{ scala.collection.immutable.Set.empty[A] } => Some(Set.empty[A])
case _ =>
// report.warning(s"Cannot unapply Set from ${x}\n${x.asTerm}")
None

given [A]: FromExpr[List[A]] with
def unapply(x: Expr[List[A]])(using Quotes) =
import quotes.reflect.*
val aType = x.asTerm.tpe.widen.typeArgs.head.asType
given FromExpr[A] = fromExprForType(aType).asInstanceOf[FromExpr[A]]
given Type[A] = aType.asInstanceOf[Type[A]]
x match
case '{ List[A](${ Varargs(Exprs(elems)) }*) } => Some(elems.toList)
case '{ List.empty[A] } => Some(List.empty[A])
case '{ scala.collection.immutable.List[A](${ Varargs(Exprs(elems)) }*) } => Some(elems.toList)
case '{ scala.collection.immutable.List.empty[A] } => Some(List.empty[A])
case _ =>
// report.warning(s"Cannot unapply List from ${x.show}")
None

def fromExprForType(using Quotes)(tpe: Type[?]) =
tpe match
case '[String] => summon[FromExpr[String]]
case '[Int] => summon[FromExpr[Int]]
case '[Long] => summon[FromExpr[Long]]
case '[Short] => summon[FromExpr[Short]]
case '[Char] => summon[FromExpr[Char]]
case '[Byte] => summon[FromExpr[Byte]]
case '[Double] => summon[FromExpr[Double]]
case '[Float] => summon[FromExpr[Float]]
case '[Boolean] => summon[FromExpr[Boolean]]
56 changes: 29 additions & 27 deletions modules/core/src/main/scala/neotype/ErrorMessages.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,18 @@ import scala.quoted.*
import StringFormatting.*

private[neotype] object ErrorMessages:
// contiguous ASCII dash symbol: U+2015 looks like: —

val header =
"—— Newtype Error ——————————————————————————————————————————————————————————".red
val footer =
"———————————————————————————————————————————————————————————————————————————".red

/** An error message for when the input to a Newtype's apply method is not known at compile time.
*/
def inputNotKnownAtCompileTime(using Quotes)(input: Expr[Any], nt: quotes.reflect.TypeRepr) =
def inputParseFailureMessage(using Quotes)(input: Expr[Any], nt: quotes.reflect.TypeRepr): String =
import quotes.reflect.*

val inputTpe = input.asTerm.tpe.widenTermRefByName
val example = examples(inputTpe)
val example = Calc.renderConstant(examples(input))

val newTypeNameString = nt.typeSymbol.name.replaceAll("\\$$", "").green.bold
val valueExprString = input.asTerm.pos.sourceCode.getOrElse(input.show).blue
Expand All @@ -28,7 +26,7 @@ private[neotype] object ErrorMessages:
|
| 🤠 ${"Possible Solutions".bold}
| ${"1.".dim} Try passing a literal $inputTypeString:
| $newTypeNameString(${example.show.green})
| $newTypeNameString(${example})
| ${"2.".dim} Call the ${"make".green} method, which returns a runtime-validated ${"Either".yellow}:
| $newTypeNameString.${"make".green}(${valueExprString})
| ${"3.".dim} If you are sure the input is valid, use the ${"unsafe".green} method:
Expand All @@ -40,9 +38,9 @@ private[neotype] object ErrorMessages:

/** An error message for when the compile-time validation of a Newtype's apply method fails.
*/
def validationFailed(using
def compileTimeValidationFailureMessage(using
Quotes
)(input: Expr[Any], nt: quotes.reflect.TypeRepr, source: Option[String], failureMessage: String) =
)(input: Expr[Any], nt: quotes.reflect.TypeRepr, source: Option[String], failureMessage: String): String =
import quotes.reflect.*

val isDefaultFailureMessage = failureMessage == "Validation Failed"
Expand All @@ -61,7 +59,7 @@ private[neotype] object ErrorMessages:
| $footer
|""".stripMargin

def validateIsNotInline(using Quotes)(input: Expr[Any], nt: quotes.reflect.TypeRepr): String =
def validateIsNotInlineMessage(using Quotes)(input: Expr[Any], nt: quotes.reflect.TypeRepr): String =
import quotes.reflect.*
val inputTpe = input.asTerm.tpe.widenTermRefByName
val newTypeNameString = nt.typeSymbol.name.replaceAll("\\$$", "").green.bold
Expand All @@ -77,7 +75,7 @@ private[neotype] object ErrorMessages:
| $footer
|""".stripMargin

def failedToParseCustomErrorMessage(using Quotes)(nt: quotes.reflect.TypeRepr) =
def failedToParseCustomErrorMessage(using Quotes)(nt: quotes.reflect.TypeRepr): String =
val newTypeNameString = nt.typeSymbol.name.replaceAll("\\$$", "").green.bold
s""" $header
| 😭 I've ${"FAILED".red} to parse $newTypeNameString's ${"failureMessage".green}!
Expand All @@ -90,18 +88,11 @@ private[neotype] object ErrorMessages:
| $footer
|""".stripMargin

def indent(str: String) =
str.linesIterator
.map { line =>
s" $line".blue
}
.mkString("\n")

def failedToParseValidateMethod(using
Quotes
)(input: Expr[Any], nt: quotes.reflect.TypeRepr, source: Option[String], isBodyInline: Option[Boolean]): String =
import quotes.reflect.*
if isBodyInline.contains(false) then return validateIsNotInline(input, nt)
if isBodyInline.contains(false) then return validateIsNotInlineMessage(input, nt)

val newTypeNameString = nt.typeSymbol.name.replaceAll("\\$$", "").green.bold
val sourceExpr = source.fold("") { s =>
Expand Down Expand Up @@ -129,16 +120,27 @@ private[neotype] object ErrorMessages:
| $footer
|""".stripMargin

private def indent(str: String) =
str.linesIterator
.map { line =>
s" $line".blue
}
.mkString("\n")

// Create a map from various input types to examples of the given type of statically known inputs
def examples(using Quotes)(tpe: quotes.reflect.TypeRepr) =
def examples(using Quotes)(input: Expr[Any]): Any =
import quotes.reflect.*

val examples = Map(
TypeRepr.of[String] -> '{ "foo" },
TypeRepr.of[Int] -> '{ 1 },
TypeRepr.of[Long] -> '{ 1L },
TypeRepr.of[Float] -> '{ 1.0f },
TypeRepr.of[Double] -> '{ 1.0 }
)

examples.find { case (k, _) => k <:< tpe }.get._2
input match
case '{ ($_): String } => "foo"
case '{ ($_): Int } => 1
case '{ ($_): Long } => 1L
case '{ ($_): Float } => 1.0f
case '{ ($_): Double } => 1.0
case '{ ($_): Boolean } => true
case '{ ($_): Char } => 'a'
case '{ ($_): Byte } => 1.toByte
case '{ ($_): Short } => 1.toShort
case '{ ($_): Unit } => ()
case '{ ($_): Null } => null
case _ => "input"
62 changes: 47 additions & 15 deletions modules/core/src/main/scala/neotype/Macros.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,15 @@ private[neotype] object Macros:
case _ =>
None

lazy val treeSource = scala.util
.Try {
lazy val treeSource =
try
nt.typeSymbol
.methodMember("validate")
.headOption
.flatMap {
_.tree match
case body =>
body.pos.sourceCode
case _ =>
None
_.tree.pos.sourceCode
}
}
.toOption
.flatten
catch case _: Throwable => None

val isBodyInline = nt.typeSymbol
.methodMember("validate")
Expand All @@ -48,25 +42,28 @@ private[neotype] object Macros:
case Calc[A](calc) =>
scala.util.Try(calc.result(using Map.empty)) match
case Failure(_) =>
report.errorAndAbort(ErrorMessages.inputNotKnownAtCompileTime(a, nt))
report.errorAndAbort(ErrorMessages.inputParseFailureMessage(a, nt))
case Success(_) =>
()
case _ =>
report.errorAndAbort(ErrorMessages.inputNotKnownAtCompileTime(a, nt))
report.errorAndAbort(ErrorMessages.inputParseFailureMessage(a, nt))

val validateApplied = Expr.betaReduce('{ $validate($a) })
validateApplied match
case Calc(calc) =>
case Calc[A](calc) =>
scala.util.Try(calc.result(using Map.empty)) match
case Failure(exception) =>
report.errorAndAbort(s"Failed to execute parsed validation: $exception")
// report.errorAndAbort(s"Failed to execute parsed validation: $exception")
report.errorAndAbort(ErrorMessages.failedToParseValidateMethod(a, nt, treeSource, isBodyInline))
case Success(true) =>
a.asExprOf[T]
case Success(false) =>
val failureMessageValue = failureMessage match
case Expr(str: String) => str
case _ => "Validation Failed"
report.errorAndAbort(ErrorMessages.validationFailed(a, nt, expressionSource, failureMessageValue))
report.errorAndAbort(
ErrorMessages.compileTimeValidationFailureMessage(a, nt, expressionSource, failureMessageValue)
)
case _ =>
report.errorAndAbort(ErrorMessages.failedToParseValidateMethod(a, nt, treeSource, isBodyInline))

Expand All @@ -89,3 +86,38 @@ private[neotype] object Macros:
processArgs(args.asInstanceOf[Seq[Expr[A]]])
case other =>
report.errorAndAbort(s"Could not parse input at compile time: ${other.show}")

private[neotype] object TestMacros:
inline def eval[A](inline expr: A): A = ${ evalImpl[A]('expr) }
inline def evalDebug[A](inline expr: A): A = ${ evalDebugImpl[A]('expr) }

def evalDebugImpl[A: Type](using Quotes)(expr: Expr[A]): Expr[A] =
import quotes.reflect.*
report.info(s"expr: ${expr.show}\nterm: ${expr.asTerm.underlyingArgument}")
evalImpl(expr)

def evalImpl[A: Type](using Quotes)(expr: Expr[A]): Expr[A] =
import quotes.reflect.*
expr match
case Calc[A](calc) =>
val result = calc.result(using Map.empty)
given ToExpr[A] = toExprInstance(result).asInstanceOf[ToExpr[A]]
Expr(result)
case _ =>
report.errorAndAbort(s"Could not parse input at compile time: ${expr.show}\n\n${expr.asTerm.toString.blue}")
???

def toExprInstance(using Quotes)(any: Any): ToExpr[?] =
import quotes.reflect.*
any match
case _: Int => summon[ToExpr[Int]]
case _: String => summon[ToExpr[String]]
case _: Boolean => summon[ToExpr[Boolean]]
case _: Long => summon[ToExpr[Long]]
case _: Double => summon[ToExpr[Double]]
case _: Float => summon[ToExpr[Float]]
case _: Char => summon[ToExpr[Char]]
case _: Byte => summon[ToExpr[Byte]]
case _: Short => summon[ToExpr[Short]]
case _: Set[Int] => summon[ToExpr[Set[Int]]]
case _: List[Int] => summon[ToExpr[List[Int]]]
Loading

0 comments on commit 61d8670

Please sign in to comment.