From 4dc61c8a03605c887b0350f93ceb75036410e542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Fromentin?= <42907886+Iltotore@users.noreply.github.com> Date: Wed, 19 Feb 2025 15:18:02 +0100 Subject: [PATCH] feat: Add remaining Array support (#292) Continuation of #280. Closes #279 --- .../io/github/iltotore/iron/compileTime.scala | 29 +++- .../iltotore/iron/constraint/collection.scala | 138 +++++++++++++++++- 2 files changed, 165 insertions(+), 2 deletions(-) diff --git a/main/src/io/github/iltotore/iron/compileTime.scala b/main/src/io/github/iltotore/iron/compileTime.scala index 643d584..146a166 100644 --- a/main/src/io/github/iltotore/iron/compileTime.scala +++ b/main/src/io/github/iltotore/iron/compileTime.scala @@ -5,6 +5,7 @@ import scala.compiletime.ops.* import scala.compiletime.ops.any.ToString import scala.quoted.* import scala.annotation.targetName +import scala.reflect.ClassTag /** * Methods and types to ease compile-time operations. @@ -214,4 +215,30 @@ object compileTime: case '{ scala.collection.immutable.List[T](${Varargs(elems)}*) } => Some(elems.toList) case '{ scala.collection.immutable.List.empty[T] } => Some(Nil) case _ => None - \ No newline at end of file + + extension [T : Type](expr: Expr[Array[T]]) + + def toExprListArr(using Quotes): Option[List[Expr[T]]] = + import quotes.reflect.* + + expr match + case '{ scala.Array[T](${Varargs(elems)}*)(using $_) } => Some(elems.toList) + case '{ scala.Array.empty[T](using $_) } => Some(Nil) + case '{ scala.Array[T](${Varargs(elems)}*)(using $_) } => Some(elems.toList) + case '{ scala.Array.empty[T](using $_) } => Some(Nil) + case expr => + def stripInlinedTyped(term: Term): Term = term match + case Inlined(_, Nil, t) => stripInlinedTyped(t) + case Typed(t, _) => stripInlinedTyped(t) + case _ => term + + stripInlinedTyped(expr.asTerm) match + case Apply(Select(Ident("Array"), "apply"), List(head, repeated)) => + stripInlinedTyped(repeated) match + case Repeated(tail, _) => + val args = head :: tail + Some(args.map(_.asExprOf[T])) + case _ => None + case _ => None + + \ No newline at end of file diff --git a/main/src/io/github/iltotore/iron/constraint/collection.scala b/main/src/io/github/iltotore/iron/constraint/collection.scala index 7ae8f8e..558a3ae 100644 --- a/main/src/io/github/iltotore/iron/constraint/collection.scala +++ b/main/src/io/github/iltotore/iron/constraint/collection.scala @@ -210,6 +210,14 @@ object collection: inline given forAllString[C, Impl <: Constraint[Char, C]](using inline impl: Impl): ForAllString[C, Impl] = new ForAllString + class ForAllArray[A, C, Impl <: Constraint[A, C]](using Impl) extends Constraint[Array[A], ForAll[C]]: + + override inline def test(inline value: Array[A]): Boolean = ${ checkArray('value, '{ summonInline[Impl] }) } + + override inline def message: String = "For each element: (" + summonInline[Impl].message + ")" + + inline given forAllArray[A, C, Impl <: Constraint[A, C]](using inline impl: Impl): ForAllArray[A, C, Impl] = new ForAllArray + 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.* @@ -235,6 +243,21 @@ object collection: case _ => '{ $expr.forallOptimized(c => ${ applyConstraint('c, constraintExpr) }) } + private def checkArray[A : Type, C, Impl <: Constraint[A, C]](expr: Expr[Array[A]], constraintExpr: Expr[Impl])(using Quotes): Expr[Boolean] = + val rflUtil = reflectUtil + import rflUtil.* + import quotes.reflect.* + + expr.toExprListArr match + case Some(array) => + array + .map(applyConstraint(_, constraintExpr)) + .foldLeft(Expr(true))((e, t) => '{ $e && $t }) + + case None => + report.info(s"Cannot decode ${expr.asTerm.show(using Printer.TreeStructure)}") + '{ $expr.forall(c => ${ applyConstraint('c, constraintExpr) }) } + given [C1, C2](using C1 ==> C2): (ForAll[C1] ==> Exists[C2]) = Implication() given [C1, C2](using C1 ==> C2): (ForAll[C1] ==> Last[C2]) = Implication() given [C1, C2](using C1 ==> C2): (ForAll[C1] ==> Init[C2]) = Implication() @@ -259,6 +282,15 @@ object collection: inline given initString[C, Impl <: Constraint[Char, C]](using inline impl: Impl): InitString[C, Impl] = new InitString + class InitArray[A, C, Impl <: Constraint[A, C]](using Impl) extends Constraint[Array[A], Init[C]]: + + override inline def test(inline value: Array[A]): Boolean = ${ checkArray('value, '{ summonInline[Impl] }) } + + override inline def message: String = "For each element except last: (" + summonInline[Impl].message + ")" + + inline given [A, C, Impl <: Constraint[A, C]](using inline impl: Impl): InitArray[A, C, Impl] = + new InitArray + 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.* @@ -289,6 +321,22 @@ object collection: case _ => '{ $expr.init.forallOptimized(c => ${ applyConstraint('c, constraintExpr) }) } + private def checkArray[A : Type, C, Impl <: Constraint[A, C]](expr: Expr[Array[A]], constraintExpr: Expr[Impl])(using Quotes): Expr[Boolean] = + val rflUtil = reflectUtil + import rflUtil.* + + expr.toExprListArr 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) }) } + given [C1, C2](using C1 ==> C2): (Init[C1] ==> Exists[C2]) = Implication() given [C1, C2](using C1 ==> C2): (Init[C1] ==> Head[C2]) = Implication() @@ -310,6 +358,15 @@ object collection: override inline def message: String = "For each element except head: (" + summonInline[Impl].message + ")" + class TailArray[A, C, Impl <: Constraint[A, C]](using Impl) extends Constraint[Array[A], Tail[C]]: + + override inline def test(inline value: Array[A]): Boolean = ${ checkArray('value, '{ summonInline[Impl] }) } + + override inline def message: String = "For each element except head: (" + summonInline[Impl].message + ")" + + inline given [A, C, Impl <: Constraint[A, C]](using inline impl: Impl): TailArray[A, C, Impl] = + new TailArray + 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] = @@ -342,6 +399,22 @@ object collection: case _ => '{ $expr.tail.forallOptimized(c => ${ applyConstraint('c, constraintExpr) }) } + private def checkArray[A : Type, C, Impl <: Constraint[A, C]](expr: Expr[Array[A]], constraintExpr: Expr[Impl])(using Quotes): Expr[Boolean] = + val rflUtil = reflectUtil + import rflUtil.* + + expr.toExprListArr 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) }) } + given [C1, C2](using C1 ==> C2): (Tail[C1] ==> Exists[C2]) = Implication() given [C1, C2](using C1 ==> C2): (Tail[C1] ==> Last[C2]) = Implication() @@ -361,8 +434,17 @@ object collection: override inline def test(inline value: String): Boolean = ${ checkString('value, '{ summonInline[Impl] }) } override inline def message: String = "At least one element: (" + summonInline[Impl].message + ")" - + inline given existsString[C, Impl <: Constraint[Char, C]](using inline impl: Impl): ExistsString[C, Impl] = new ExistsString + + class ExistsArray[A, C, Impl <: Constraint[A, C]](using Impl) extends Constraint[Array[A], Exists[C]]: + + override inline def test(inline value: Array[A]): Boolean = ${ checkArray('value, '{ summonInline[Impl] }) } + + override inline def message: String = "At least one: (" + summonInline[Impl].message + ")" + + inline given [A, C, Impl <: Constraint[A, C]](using inline impl: Impl): ExistsArray[A, C, Impl] = + new ExistsArray 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 @@ -389,6 +471,18 @@ object collection: case _ => '{ $expr.existsOptimized(c => ${ applyConstraint('c, constraintExpr) }) } + private def checkArray[A : Type, C, Impl <: Constraint[A, C]](expr: Expr[Array[A]], constraintExpr: Expr[Impl])(using Quotes): Expr[Boolean] = + val rflUtil = reflectUtil + import rflUtil.* + + expr.toExprListArr match + case Some(list) => + list + .map(applyConstraint(_, constraintExpr)) + .foldLeft(Expr(false))((e, t) => '{ $e || $t }) + + case None => '{ $expr.exists(c => ${ applyConstraint('c, constraintExpr) }) } + object Head: class HeadIterable[A, I <: Iterable[A], C, Impl <: Constraint[A, C]](using Impl) extends Constraint[I, Head[C]]: @@ -408,6 +502,15 @@ object collection: inline given headString[C, Impl <: Constraint[Char, C]](using inline impl: Impl): HeadString[C, Impl] = new HeadString + class HeadArray[A, C, Impl <: Constraint[A, C]](using Impl) extends Constraint[Array[A], Head[C]]: + + override inline def test(inline value: Array[A]): Boolean = ${ checkArray('value, '{ summonInline[Impl] }) } + + override inline def message: String = "Head: (" + summonInline[Impl].message + ")" + + inline given [A, I <: Iterable[A], C, Impl <: Constraint[A, C]](using inline impl: Impl): HeadArray[A, C, Impl] = + new HeadArray + 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.* @@ -431,6 +534,18 @@ object collection: case None => Expr(false) case _ => '{ $expr.headOption.exists(head => ${ applyConstraint('{ head }, constraintExpr) }) } + private def checkArray[A : Type, C, Impl <: Constraint[A, C]](expr: Expr[Array[A]], constraintExpr: Expr[Impl])(using Quotes): Expr[Boolean] = + val rflUtil = reflectUtil + import rflUtil.* + + expr.toExprListArr 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) }) } + given [C1, C2](using C1 ==> C2): (Head[C1] ==> Exists[C2]) = Implication() object Last: @@ -450,6 +565,15 @@ object collection: override inline def message: String = "Last: (" + summonInline[Impl].message + ")" + class LastArray[A, C, Impl <: Constraint[A, C]](using Impl) extends Constraint[Array[A], Last[C]]: + + override inline def test(inline value: Array[A]): Boolean = ${ checkArray('value, '{ summonInline[Impl] }) } + + override inline def message: String = "Last: (" + summonInline[Impl].message + ")" + + inline given [A, C, Impl <: Constraint[A, C]](using inline impl: Impl): LastArray[A, C, Impl] = + new LastArray + 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] = @@ -475,6 +599,18 @@ object collection: case None => Expr(false) case _ => '{ $expr.lastOption.exists(last => ${ applyConstraint('{ last }, constraintExpr) }) } + private def checkArray[A : Type, C, Impl <: Constraint[A, C]](expr: Expr[Array[A]], constraintExpr: Expr[Impl])(using Quotes): Expr[Boolean] = + val rflUtil = reflectUtil + import rflUtil.* + + expr.toExprListArr 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) }) } + given [C1, C2](using C1 ==> C2): (Last[C1] ==> Exists[C2]) = Implication() /**