Skip to content

Commit

Permalink
feat: Add intermediate List/Set decode (#269)
Browse files Browse the repository at this point in the history
Allow intermerdiate decoding of `List[T]` and `Set[T]` exprs, enabling
compile-time support for all `collection` constraints and making
`Length` more flexible.
  • Loading branch information
Iltotore authored Sep 20, 2024
1 parent a55d33c commit 576dbaa
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 14 deletions.
16 changes: 16 additions & 0 deletions main/src/io/github/iltotore/iron/compileTime.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import scala.compiletime.constValue
import scala.compiletime.ops.*
import scala.compiletime.ops.any.ToString
import scala.quoted.*
import scala.annotation.targetName

/**
* Methods and types to ease compile-time operations.
Expand Down Expand Up @@ -199,3 +200,18 @@ object compileTime:
import quotes.reflect.*

Apply(Select.unique(constraintExpr.asTerm, "test"), List(expr.asTerm)).asExprOf[Boolean]

extension [T : Type](expr: Expr[Iterable[T]])

def toExprList(using Quotes): Option[List[Expr[T]]] = expr match
case '{ scala.List[T](${Varargs(elems)}*) } => Some(elems.toList)
case '{ scala.List.empty[T] } => Some(Nil)
case '{ Nil } => Some(Nil)
case '{ scala.collection.immutable.List[T](${Varargs(elems)}*) } => Some(elems.toList)
case '{ scala.collection.immutable.List.empty[T] } => Some(Nil)
case '{ Set[T](${Varargs(elems)}*) } => Some(elems.toList)
case '{ Set.empty[T] } => Some(Nil)
case '{ scala.collection.immutable.List[T](${Varargs(elems)}*) } => Some(elems.toList)
case '{ scala.collection.immutable.List.empty[T] } => Some(Nil)
case _ => None

102 changes: 91 additions & 11 deletions main/src/io/github/iltotore/iron/constraint/collection.scala
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ object collection:
val rflUtil = reflectUtil
import rflUtil.*

expr.decode match
case Right(value) => applyConstraint(Expr(value.size), constraintExpr)
expr.toExprList match
case Some(list) => applyConstraint(Expr(list.size), constraintExpr)
case _ => applyConstraint('{ $expr.size }, constraintExpr)

private def checkString[C, Impl <: Constraint[Int, C]](expr: Expr[String], constraintExpr: Expr[Impl])(using Quotes): Expr[Boolean] =
Expand Down Expand Up @@ -164,7 +164,7 @@ object collection:

class ForAllIterable[A, I <: Iterable[A], C, Impl <: Constraint[A, C]](using Impl) extends Constraint[I, ForAll[C]]:

override inline def test(inline value: I): Boolean = value.forall(summonInline[Impl].test(_))
override inline def test(inline value: I): Boolean = ${ checkIterable('value, '{ summonInline[Impl] }) }

override inline def message: String = "For each element: (" + summonInline[Impl].message + ")"

Expand All @@ -179,6 +179,18 @@ object collection:

inline given forAllString[C, Impl <: Constraint[Char, C]](using inline impl: Impl): ForAllString[C, Impl] = new ForAllString

private def checkIterable[A : Type, I <: Iterable[A] : Type, C, Impl <: Constraint[A, C]](expr: Expr[I], constraintExpr: Expr[Impl])(using Quotes): Expr[Boolean] =
val rflUtil = reflectUtil
import rflUtil.*

expr.toExprList match
case Some(list) =>
list
.map(applyConstraint(_, constraintExpr))
.foldLeft(Expr(true))((e, t) => '{ $e && $t })

case None => '{ $expr.forall(c => ${ applyConstraint('c, constraintExpr) }) }

private def checkString[C, Impl <: Constraint[Char, C]](expr: Expr[String], constraintExpr: Expr[Impl])(using Quotes): Expr[Boolean] =
val rflUtil = reflectUtil
import rflUtil.*
Expand All @@ -201,9 +213,9 @@ object collection:

class InitIterable[A, I <: Iterable[A], C, Impl <: Constraint[A, C]](using Impl) extends Constraint[I, Init[C]]:

override inline def test(inline value: I): Boolean = value.isEmpty || value.init.forall(summonInline[Impl].test(_))
override inline def test(inline value: I): Boolean = ${ checkIterable('value, '{ summonInline[Impl] }) }

override inline def message: String = "For each element except head: (" + summonInline[Impl].message + ")"
override inline def message: String = "For each element except last: (" + summonInline[Impl].message + ")"

inline given [A, I <: Iterable[A], C, Impl <: Constraint[A, C]](using inline impl: Impl): InitIterable[A, I, C, Impl] =
new InitIterable
Expand All @@ -216,6 +228,22 @@ object collection:

inline given initString[C, Impl <: Constraint[Char, C]](using inline impl: Impl): InitString[C, Impl] = new InitString

private def checkIterable[A : Type, I <: Iterable[A] : Type, C, Impl <: Constraint[A, C]](expr: Expr[I], constraintExpr: Expr[Impl])(using Quotes): Expr[Boolean] =
val rflUtil = reflectUtil
import rflUtil.*

expr.toExprList match
case Some(list) =>
list match
case Nil => Expr(true)
case _ =>
list
.init
.map(applyConstraint(_, constraintExpr))
.foldLeft(Expr(true))((e, t) => '{ $e && $t })

case None => '{ $expr.init.forall(c => ${ applyConstraint('c, constraintExpr) }) }

private def checkString[C, Impl <: Constraint[Char, C]](expr: Expr[String], constraintExpr: Expr[Impl])(using Quotes): Expr[Boolean] =
val rflUtil = reflectUtil
import rflUtil.*
Expand All @@ -238,9 +266,9 @@ object collection:

class TailIterable[A, I <: Iterable[A], C, Impl <: Constraint[A, C]](using Impl) extends Constraint[I, Tail[C]]:

override inline def test(inline value: I): Boolean = value.isEmpty || value.tail.forall(summonInline[Impl].test(_))
override inline def test(inline value: I): Boolean = ${ checkIterable('value, '{ summonInline[Impl] }) }

override inline def message: String = "For each element: (" + summonInline[Impl].message + ")"
override inline def message: String = "For each element except head: (" + summonInline[Impl].message + ")"

inline given [A, I <: Iterable[A], C, Impl <: Constraint[A, C]](using inline impl: Impl): TailIterable[A, I, C, Impl] =
new TailIterable
Expand All @@ -249,10 +277,26 @@ object collection:

override inline def test(inline value: String): Boolean = ${ checkString('value, '{ summonInline[Impl] }) }

override inline def message: String = "For each element: (" + summonInline[Impl].message + ")"
override inline def message: String = "For each element except head: (" + summonInline[Impl].message + ")"

inline given tailString[C, Impl <: Constraint[Char, C]](using inline impl: Impl): TailString[C, Impl] = new TailString

private def checkIterable[A : Type, I <: Iterable[A] : Type, C, Impl <: Constraint[A, C]](expr: Expr[I], constraintExpr: Expr[Impl])(using Quotes): Expr[Boolean] =
val rflUtil = reflectUtil
import rflUtil.*

expr.toExprList match
case Some(list) =>
list match
case Nil => Expr(true)
case _ =>
list
.tail
.map(applyConstraint(_, constraintExpr))
.foldLeft(Expr(true))((e, t) => '{ $e && $t })

case None => '{ $expr.tail.forall(c => ${ applyConstraint('c, constraintExpr) }) }

private def checkString[C, Impl <: Constraint[Char, C]](expr: Expr[String], constraintExpr: Expr[Impl])(using Quotes): Expr[Boolean] =
val rflUtil = reflectUtil
import rflUtil.*
Expand All @@ -274,7 +318,7 @@ object collection:

class ExistsIterable[A, I <: Iterable[A], C, Impl <: Constraint[A, C]](using Impl) extends Constraint[I, Exists[C]]:

override inline def test(inline value: I): Boolean = value.exists(summonInline[Impl].test(_))
override inline def test(inline value: I): Boolean = ${ checkIterable('value, '{ summonInline[Impl] }) }

override inline def message: String = "At least one: (" + summonInline[Impl].message + ")"

Expand All @@ -289,6 +333,18 @@ object collection:

inline given existsString[C, Impl <: Constraint[Char, C]](using inline impl: Impl): ExistsString[C, Impl] = new ExistsString

private def checkIterable[A : Type, I <: Iterable[A] : Type, C, Impl <: Constraint[A, C]](expr: Expr[I], constraintExpr: Expr[Impl])(using Quotes): Expr[Boolean] =
val rflUtil = reflectUtil
import rflUtil.*

expr.toExprList match
case Some(list) =>
list
.map(applyConstraint(_, constraintExpr))
.foldLeft(Expr(false))((e, t) => '{ $e || $t })

case None => '{ $expr.exists(c => ${ applyConstraint('c, constraintExpr) }) }

private def checkString[C, Impl <: Constraint[Char, C]](expr: Expr[String], constraintExpr: Expr[Impl])(using Quotes): Expr[Boolean] =
val rflUtil = reflectUtil
import rflUtil.*
Expand All @@ -306,7 +362,7 @@ object collection:

class HeadIterable[A, I <: Iterable[A], C, Impl <: Constraint[A, C]](using Impl) extends Constraint[I, Head[C]]:

override inline def test(inline value: I): Boolean = value.headOption.exists(summonInline[Impl].test(_))
override inline def test(inline value: I): Boolean = ${ checkIterable('value, '{ summonInline[Impl] }) }

override inline def message: String = "Head: (" + summonInline[Impl].message + ")"

Expand All @@ -321,6 +377,18 @@ object collection:

inline given headString[C, Impl <: Constraint[Char, C]](using inline impl: Impl): HeadString[C, Impl] = new HeadString

private def checkIterable[A : Type, I <: Iterable[A] : Type, C, Impl <: Constraint[A, C]](expr: Expr[I], constraintExpr: Expr[Impl])(using Quotes): Expr[Boolean] =
val rflUtil = reflectUtil
import rflUtil.*

expr.toExprList match
case Some(list) =>
list.headOption match
case Some(head) => applyConstraint(head, constraintExpr)
case None => Expr(false)

case None => '{ $expr.headOption.exists(c => ${ applyConstraint('c, constraintExpr) }) }

private def checkString[C, Impl <: Constraint[Char, C]](expr: Expr[String], constraintExpr: Expr[Impl])(using Quotes): Expr[Boolean] =
val rflUtil = reflectUtil
import rflUtil.*
Expand All @@ -338,7 +406,7 @@ object collection:

class LastIterable[A, I <: Iterable[A], C, Impl <: Constraint[A, C]](using Impl) extends Constraint[I, Last[C]]:

override inline def test(inline value: I): Boolean = value.lastOption.exists(summonInline[Impl].test(_))
override inline def test(inline value: I): Boolean = ${ checkIterable('value, '{ summonInline[Impl] }) }

override inline def message: String = "Last: (" + summonInline[Impl].message + ")"

Expand All @@ -353,6 +421,18 @@ object collection:

inline given lastString[C, Impl <: Constraint[Char, C]](using inline impl: Impl): LastString[C, Impl] = new LastString

private def checkIterable[A : Type, I <: Iterable[A] : Type, C, Impl <: Constraint[A, C]](expr: Expr[I], constraintExpr: Expr[Impl])(using Quotes): Expr[Boolean] =
val rflUtil = reflectUtil
import rflUtil.*

expr.toExprList match
case Some(list) =>
list.lastOption match
case Some(last) => applyConstraint(last, constraintExpr)
case None => Expr(false)

case None => '{ $expr.lastOption.exists(c => ${ applyConstraint('c, constraintExpr) }) }

private def checkString[C, Impl <: Constraint[Char, C]](expr: Expr[String], constraintExpr: Expr[Impl])(using Quotes): Expr[Boolean] =
val rflUtil = reflectUtil
import rflUtil.*
Expand Down
3 changes: 1 addition & 2 deletions main/src/io/github/iltotore/iron/macros/ReflectUtil.scala
Original file line number Diff line number Diff line change
Expand Up @@ -390,5 +390,4 @@ class ReflectUtil[Q <: Quotes & Singleton](using val _quotes: Q):
term match
case Apply(TypeApply(Select(Ident("Set"), "apply"), _), List(values)) =>
decodeTerm(values, definitions).as[List[?]].map(_.toSet)
case _ => Left(DecodingFailure.Unknown)

case _ => Left(DecodingFailure.Unknown)
2 changes: 1 addition & 1 deletion main/src/io/github/iltotore/iron/macros/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,4 @@ def isIronTypeImpl[T: Type, C: Type](using Quotes): Expr[Boolean] =
case _: ImplicitSearchSuccess => Expr(true)
case _: ImplicitSearchFailure => Expr(false)

case _ => Expr(false)
case _ => Expr(false)

0 comments on commit 576dbaa

Please sign in to comment.