From a55d33c2a60172952e7d2f45c929212c45458dce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Fromentin?= <42907886+Iltotore@users.noreply.github.com> Date: Sat, 14 Sep 2024 11:36:05 +0200 Subject: [PATCH] feat: Add compile-time support for non-primitive types (#266) Closes #147 --- docs/_docs/reference/constraint.md | 8 +- .../io/github/iltotore/iron/Constraint.scala | 6 +- .../iltotore/iron/RuntimeConstraint.scala | 2 +- .../github/iltotore/iron/constraint/any.scala | 18 +-- .../iltotore/iron/constraint/char.scala | 10 +- .../iltotore/iron/constraint/collection.scala | 52 +++++-- .../iltotore/iron/constraint/numeric.scala | 147 ++++++++++++++---- .../iltotore/iron/constraint/string.scala | 6 +- .../iltotore/iron/macros/ReflectUtil.scala | 128 +++++++++++---- .../iron/testing/CollectionSuite.scala | 2 +- 10 files changed, 274 insertions(+), 105 deletions(-) diff --git a/docs/_docs/reference/constraint.md b/docs/_docs/reference/constraint.md index 044f97ec..2fe04217 100644 --- a/docs/_docs/reference/constraint.md +++ b/docs/_docs/reference/constraint.md @@ -70,7 +70,7 @@ import io.github.iltotore.iron.* //} given Constraint[Int, Positive] with - override inline def test(value: Int): Boolean = value > 0 + override inline def test(inline value: Int): Boolean = value > 0 override inline def message: String = "Should be strictly positive" ``` @@ -87,10 +87,10 @@ trait PositiveConstraint[A] extends Constraint[A, Positive]: override inline def message: String = "Should be strictly positive" given PositiveConstraint[Int] with - override inline def test(value: Int): Boolean = value > 0 + override inline def test(inline value: Int): Boolean = value > 0 given PositiveConstraint[Double] with - override inline def test(value: Double): Boolean = value > 0.0 + override inline def test(inline value: Double): Boolean = value > 0.0 ``` This constraint can now be used like any other: @@ -121,7 +121,7 @@ import scala.compiletime.constValue given [V]: Constraint[Int, Greater[V]] with - override inline def test(value: Int): Boolean = value > constValue[V] + override inline def test(inline value: Int): Boolean = value > constValue[V] override inline def message: String = "Should be greater than " + stringValue[V] ``` diff --git a/main/src/io/github/iltotore/iron/Constraint.scala b/main/src/io/github/iltotore/iron/Constraint.scala index 65644974..5a6f3e0b 100644 --- a/main/src/io/github/iltotore/iron/Constraint.scala +++ b/main/src/io/github/iltotore/iron/Constraint.scala @@ -11,14 +11,14 @@ import io.github.iltotore.iron.macros.intersection.* */ trait Constraint[A, C]: - inline def test(value: A): Boolean + inline def test(inline value: A): Boolean inline def message: String object Constraint: class UnionConstraint[A, C] extends Constraint[A, C]: - override inline def test(value: A): Boolean = unionCond[A, C](value) + override inline def test(inline value: A): Boolean = unionCond[A, C](value) override inline def message: String = unionMessage[A, C] @@ -26,7 +26,7 @@ object Constraint: class IntersectionConstraint[A, C] extends Constraint[A, C]: - override inline def test(value: A): Boolean = intersectionCond[A, C](value) + override inline def test(inline value: A): Boolean = intersectionCond[A, C](value) override inline def message: String = intersectionMessage[A, C] diff --git a/main/src/io/github/iltotore/iron/RuntimeConstraint.scala b/main/src/io/github/iltotore/iron/RuntimeConstraint.scala index 3236dc60..e5e875bf 100644 --- a/main/src/io/github/iltotore/iron/RuntimeConstraint.scala +++ b/main/src/io/github/iltotore/iron/RuntimeConstraint.scala @@ -17,7 +17,7 @@ import scala.util.NotGiven * In cases that one does not exist in scope, one will be automatically derived from a [[Constraint]]. */ final class RuntimeConstraint[A, C](_test: A => Boolean, val message: String): - inline def test(value: A): Boolean = _test(value) + inline def test(inline value: A): Boolean = _test(value) object RuntimeConstraint: inline given derived[A, C](using inline c: Constraint[A, C]): RuntimeConstraint[A, C] = diff --git a/main/src/io/github/iltotore/iron/constraint/any.scala b/main/src/io/github/iltotore/iron/constraint/any.scala index 5d584fb9..8a2beee7 100644 --- a/main/src/io/github/iltotore/iron/constraint/any.scala +++ b/main/src/io/github/iltotore/iron/constraint/any.scala @@ -88,7 +88,7 @@ object any: inline given [A]: Constraint[A, True] with - override inline def test(value: A): Boolean = true + override inline def test(inline value: A): Boolean = true override inline def message: String = "Always valid" @@ -101,7 +101,7 @@ object any: inline given [A]: Constraint[A, False] with - override inline def test(value: A): Boolean = false + override inline def test(inline value: A): Boolean = false override inline def message: String = "Always invalid" @@ -113,7 +113,7 @@ object any: object DescribedAs: class DescribedAsConstraint[A, C, Impl <: Constraint[A, C], V <: String](using Impl) extends Constraint[A, DescribedAs[C, V]]: - override inline def test(value: A): Boolean = summonInline[Impl].test(value) + override inline def test(inline value: A): Boolean = summonInline[Impl].test(value) override inline def message: String = constValue[V] @@ -133,7 +133,7 @@ object any: object Not: class NotConstraint[A, C, Impl <: Constraint[A, C]](using Impl) extends Constraint[A, Not[C]]: - override inline def test(value: A): Boolean = + override inline def test(inline value: A): Boolean = !summonInline[Impl].test(value) override inline def message: String = @@ -157,7 +157,7 @@ object any: class XorConstraint[A, C1, C2, Impl1 <: Constraint[A, C1], Impl2 <: Constraint[A, C2]] extends Constraint[A, Xor[C1, C2]]: - override inline def test(value: A): Boolean = summonInline[Impl1].test(value) != summonInline[Impl2].test(value) + override inline def test(inline value: A): Boolean = summonInline[Impl1].test(value) != summonInline[Impl2].test(value) override inline def message: String = "(" + summonInline[Impl1].message + " xor " + summonInline[Impl2].message + ")" @@ -188,13 +188,13 @@ object any: override inline def message: String = "Should strictly equal to " + stringValue[V] inline given [A, V]: StrictEqualConstraint[A, V] with - override inline def test(value: A): Boolean = value == constValue[V] + override inline def test(inline value: A): Boolean = value == constValue[V] inline given bigDecimalDouble[V <: Float | Double]: StrictEqualConstraint[BigDecimal, V] with - override inline def test(value: BigDecimal): Boolean = value == BigDecimal(doubleValue[V]) + override inline def test(inline value: BigDecimal): Boolean = value == BigDecimal(doubleValue[V]) inline given bigDecimalLong[V <: Int | Long]: StrictEqualConstraint[BigDecimal, V] with - override inline def test(value: BigDecimal): Boolean = value == BigDecimal(longValue[V]) + override inline def test(inline value: BigDecimal): Boolean = value == BigDecimal(longValue[V]) inline given [V <: Int | Long]: StrictEqualConstraint[BigInt, V] with - override inline def test(value: BigInt): Boolean = value == BigInt(longValue[V]) + override inline def test(inline value: BigInt): Boolean = value == BigInt(longValue[V]) diff --git a/main/src/io/github/iltotore/iron/constraint/char.scala b/main/src/io/github/iltotore/iron/constraint/char.scala index d66ec66c..8a91306b 100644 --- a/main/src/io/github/iltotore/iron/constraint/char.scala +++ b/main/src/io/github/iltotore/iron/constraint/char.scala @@ -43,7 +43,7 @@ object char: inline given Constraint[Char, Whitespace] with - override inline def test(value: Char): Boolean = ${ check('value) } + override inline def test(inline value: Char): Boolean = ${ check('value) } override inline def message: String = "Should be a whitespace" @@ -59,7 +59,7 @@ object char: inline given Constraint[Char, LowerCase] with - override inline def test(value: Char): Boolean = ${ check('value) } + override inline def test(inline value: Char): Boolean = ${ check('value) } override inline def message: String = "Should be a lower cased" @@ -75,7 +75,7 @@ object char: inline given Constraint[Char, UpperCase] with - override inline def test(value: Char): Boolean = ${ check('value) } + override inline def test(inline value: Char): Boolean = ${ check('value) } override inline def message: String = "Should be a upper cased" @@ -91,7 +91,7 @@ object char: inline given Constraint[Char, Digit] with - override inline def test(value: Char): Boolean = ${ check('value) } + override inline def test(inline value: Char): Boolean = ${ check('value) } override inline def message: String = "Should be a digit" @@ -107,7 +107,7 @@ object char: inline given Constraint[Char, Letter] with - override inline def test(value: Char): Boolean = ${ check('value) } + override inline def test(inline value: Char): Boolean = ${ check('value) } override inline def message: String = "Should be a letter" diff --git a/main/src/io/github/iltotore/iron/constraint/collection.scala b/main/src/io/github/iltotore/iron/constraint/collection.scala index 034ec2c4..06dccafc 100644 --- a/main/src/io/github/iltotore/iron/constraint/collection.scala +++ b/main/src/io/github/iltotore/iron/constraint/collection.scala @@ -96,7 +96,7 @@ object collection: class LengthIterable[I <: Iterable[?], C, Impl <: Constraint[Int, C]](using Impl) extends Constraint[I, Length[C]]: - override inline def test(value: I): Boolean = summonInline[Impl].test(value.size) + override inline def test(inline value: I): Boolean = ${ checkIterable('value, '{ summonInline[Impl] }) } override inline def message: String = "Length: (" + summonInline[Impl].message + ")" @@ -105,12 +105,22 @@ object collection: class LengthString[C, Impl <: Constraint[Int, C]](using Impl) extends Constraint[String, Length[C]]: - override inline def test(value: String): Boolean = ${ checkString('value, '{ summonInline[Impl] }) } + override inline def test(inline value: String): Boolean = ${ checkString('value, '{ summonInline[Impl] }) } override inline def message: String = "Length: (" + summonInline[Impl].message + ")" inline given lengthString[C, Impl <: Constraint[Int, C]](using inline impl: Impl): LengthString[C, Impl] = new LengthString + private def checkIterable[I <: Iterable[?]: Type, C, Impl <: Constraint[Int, C]](expr: Expr[I], constraintExpr: Expr[Impl])(using + Quotes + ): Expr[Boolean] = + val rflUtil = reflectUtil + import rflUtil.* + + expr.decode match + case Right(value) => applyConstraint(Expr(value.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] = val rflUtil = reflectUtil import rflUtil.* @@ -124,16 +134,24 @@ object collection: object Contain: inline given [A, V <: A, I <: Iterable[A]]: Constraint[I, Contain[V]] with - override inline def test(value: I): Boolean = value.iterator.contains(constValue[V]) + override inline def test(inline value: I): Boolean = ${ checkIterable('value, '{ constValue[V] }) } - override inline def message: String = "Should contain at most " + stringValue[V] + " elements" + override inline def message: String = "Should contain the value " + stringValue[V] inline given [V <: String]: Constraint[String, Contain[V]] with - override inline def test(value: String): Boolean = ${ checkString('value, '{ constValue[V] }) } + override inline def test(inline value: String): Boolean = ${ checkString('value, '{ constValue[V] }) } override inline def message: String = "Should contain the string " + constValue[V] + private def checkIterable[I <: Iterable[?]: Type, V: Type](expr: Expr[I], partExpr: Expr[V])(using Quotes): Expr[Boolean] = + val rflUtil = reflectUtil + import rflUtil.* + + (expr.decode, partExpr.decode) match + case (Right(value), Right(part)) => Expr(value.iterator.contains(part)) + case _ => '{ ${ expr }.iterator.contains($partExpr) } + private def checkString(expr: Expr[String], partExpr: Expr[String])(using Quotes): Expr[Boolean] = val rflUtil = reflectUtil import rflUtil.* @@ -146,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(value: I): Boolean = value.forall(summonInline[Impl].test(_)) + override inline def test(inline value: I): Boolean = value.forall(summonInline[Impl].test(_)) override inline def message: String = "For each element: (" + summonInline[Impl].message + ")" @@ -155,7 +173,7 @@ object collection: class ForAllString[C, Impl <: Constraint[Char, C]](using Impl) extends Constraint[String, ForAll[C]]: - override inline def test(value: String): Boolean = ${ checkString('value, '{ summonInline[Impl] }) } + override inline def test(inline value: String): Boolean = ${ checkString('value, '{ summonInline[Impl] }) } override inline def message: String = "For each element: (" + summonInline[Impl].message + ")" @@ -183,7 +201,7 @@ object collection: class InitIterable[A, I <: Iterable[A], C, Impl <: Constraint[A, C]](using Impl) extends Constraint[I, Init[C]]: - override inline def test(value: I): Boolean = value.isEmpty || value.init.forall(summonInline[Impl].test(_)) + override inline def test(inline value: I): Boolean = value.isEmpty || value.init.forall(summonInline[Impl].test(_)) override inline def message: String = "For each element except head: (" + summonInline[Impl].message + ")" @@ -192,7 +210,7 @@ object collection: class InitString[C, Impl <: Constraint[Char, C]](using Impl) extends Constraint[String, Init[C]]: - override inline def test(value: String): Boolean = ${ checkString('value, '{ summonInline[Impl] }) } + override inline def test(inline value: String): Boolean = ${ checkString('value, '{ summonInline[Impl] }) } override inline def message: String = "For each element except last: (" + summonInline[Impl].message + ")" @@ -220,7 +238,7 @@ object collection: class TailIterable[A, I <: Iterable[A], C, Impl <: Constraint[A, C]](using Impl) extends Constraint[I, Tail[C]]: - override inline def test(value: I): Boolean = value.isEmpty || value.tail.forall(summonInline[Impl].test(_)) + override inline def test(inline value: I): Boolean = value.isEmpty || value.tail.forall(summonInline[Impl].test(_)) override inline def message: String = "For each element: (" + summonInline[Impl].message + ")" @@ -229,7 +247,7 @@ object collection: class TailString[C, Impl <: Constraint[Char, C]](using Impl) extends Constraint[String, Tail[C]]: - override inline def test(value: String): Boolean = ${ checkString('value, '{ summonInline[Impl] }) } + override inline def test(inline value: String): Boolean = ${ checkString('value, '{ summonInline[Impl] }) } override inline def message: String = "For each element: (" + summonInline[Impl].message + ")" @@ -256,7 +274,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(value: I): Boolean = value.exists(summonInline[Impl].test(_)) + override inline def test(inline value: I): Boolean = value.exists(summonInline[Impl].test(_)) override inline def message: String = "At least one: (" + summonInline[Impl].message + ")" @@ -265,7 +283,7 @@ object collection: class ExistsString[C, Impl <: Constraint[Char, C]](using Impl) extends Constraint[String, Exists[C]]: - override inline def test(value: String): Boolean = ${ checkString('value, '{ summonInline[Impl] }) } + override inline def test(inline value: String): Boolean = ${ checkString('value, '{ summonInline[Impl] }) } override inline def message: String = "At least one element: (" + summonInline[Impl].message + ")" @@ -288,7 +306,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(value: I): Boolean = value.headOption.exists(summonInline[Impl].test(_)) + override inline def test(inline value: I): Boolean = value.headOption.exists(summonInline[Impl].test(_)) override inline def message: String = "Head: (" + summonInline[Impl].message + ")" @@ -297,7 +315,7 @@ object collection: class HeadString[C, Impl <: Constraint[Char, C]](using Impl) extends Constraint[String, Head[C]]: - override inline def test(value: String): Boolean = ${ checkString('value, '{ summonInline[Impl] }) } + override inline def test(inline value: String): Boolean = ${ checkString('value, '{ summonInline[Impl] }) } override inline def message: String = "Head: (" + summonInline[Impl].message + ")" @@ -320,7 +338,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(value: I): Boolean = value.lastOption.exists(summonInline[Impl].test(_)) + override inline def test(inline value: I): Boolean = value.lastOption.exists(summonInline[Impl].test(_)) override inline def message: String = "Last: (" + summonInline[Impl].message + ")" @@ -329,7 +347,7 @@ object collection: class LastString[C, Impl <: Constraint[Char, C]](using Impl) extends Constraint[String, Last[C]]: - override inline def test(value: String): Boolean = ${ checkString('value, '{ summonInline[Impl] }) } + override inline def test(inline value: String): Boolean = ${ checkString('value, '{ summonInline[Impl] }) } override inline def message: String = "Last: (" + summonInline[Impl].message + ")" diff --git a/main/src/io/github/iltotore/iron/constraint/numeric.scala b/main/src/io/github/iltotore/iron/constraint/numeric.scala index f85db200..0a91ca57 100644 --- a/main/src/io/github/iltotore/iron/constraint/numeric.scala +++ b/main/src/io/github/iltotore/iron/constraint/numeric.scala @@ -3,7 +3,10 @@ package io.github.iltotore.iron.constraint import io.github.iltotore.iron.constraint.any.* import io.github.iltotore.iron.compileTime.* import io.github.iltotore.iron.{==>, Constraint, Implication} +import io.github.iltotore.iron.macros.reflectUtil +import scala.compiletime.summonInline +import scala.quoted.* import scala.util.NotGiven /** @@ -152,25 +155,49 @@ object numeric: override inline def message: String = "Should be greater than " + stringValue[V] inline given [V <: NumConstant]: GreaterConstraint[Int, V] with - override inline def test(value: Int): Boolean = value > doubleValue[V] + override inline def test(inline value: Int): Boolean = value > doubleValue[V] inline given [V <: NumConstant]: GreaterConstraint[Long, V] with - override inline def test(value: Long): Boolean = value > doubleValue[V] + override inline def test(inline value: Long): Boolean = value > doubleValue[V] inline given [V <: NumConstant]: GreaterConstraint[Float, V] with - override inline def test(value: Float): Boolean = value > doubleValue[V] + override inline def test(inline value: Float): Boolean = value > doubleValue[V] inline given [V <: NumConstant]: GreaterConstraint[Double, V] with - override inline def test(value: Double): Boolean = value > doubleValue[V] + override inline def test(inline value: Double): Boolean = value > doubleValue[V] inline given bigDecimalDouble[V <: NumConstant]: GreaterConstraint[BigDecimal, V] with - override inline def test(value: BigDecimal): Boolean = value > BigDecimal(doubleValue[V]) + override inline def test(inline value: BigDecimal): Boolean = ${ checkBigDecimalDouble('value, '{ doubleValue[V] }) } inline given bigDecimalLong[V <: Int | Long]: GreaterConstraint[BigDecimal, V] with - override inline def test(value: BigDecimal): Boolean = value > BigDecimal(longValue[V]) + override inline def test(inline value: BigDecimal): Boolean = ${ checkBigDecimalLong('value, '{ longValue[V] }) } inline given [V <: Int | Long]: GreaterConstraint[BigInt, V] with - override inline def test(value: BigInt): Boolean = value > BigInt(longValue[V]) + override inline def test(inline value: BigInt): Boolean = ${ checkBigInt('value, '{ longValue[V] }) } + + private def checkBigDecimalDouble(expr: Expr[BigDecimal], thanExpr: Expr[Double])(using Quotes): Expr[Boolean] = + val rflUtil = reflectUtil + import rflUtil.* + + (expr.decode, thanExpr.decode) match + case (Right(value), Right(than)) => Expr(value > BigDecimal(than)) + case _ => '{ $expr > BigDecimal($thanExpr) } + + private def checkBigDecimalLong(expr: Expr[BigDecimal], thanExpr: Expr[Long])(using Quotes): Expr[Boolean] = + val rflUtil = reflectUtil + import rflUtil.* + + (expr.decode, thanExpr.decode) match + case (Right(value), Right(than)) => Expr(value > BigDecimal(than)) + case _ => '{ $expr > BigDecimal($thanExpr) } + + private def checkBigInt(expr: Expr[BigInt], thanExpr: Expr[Long])(using Quotes): Expr[Boolean] = + val rflUtil = reflectUtil + import rflUtil.* + + (expr.decode, thanExpr.decode) match + case (Right(value), Right(than)) => Expr(value > BigInt(than)) + case _ => '{ $expr > BigInt($thanExpr) } given [V1, V2](using V1 > V2 =:= true): (Greater[V1] ==> Greater[V2]) = Implication() @@ -187,25 +214,49 @@ object numeric: override inline def message: String = "Should be less than " + stringValue[V] inline given [V <: NumConstant]: LessConstraint[Int, V] with - override inline def test(value: Int): Boolean = value < doubleValue[V] + override inline def test(inline value: Int): Boolean = value < doubleValue[V] inline given [V <: NumConstant]: LessConstraint[Long, V] with - override inline def test(value: Long): Boolean = value < doubleValue[V] + override inline def test(inline value: Long): Boolean = value < doubleValue[V] inline given [V <: NumConstant]: LessConstraint[Float, V] with - override inline def test(value: Float): Boolean = value < doubleValue[V] + override inline def test(inline value: Float): Boolean = value < doubleValue[V] inline given [V <: NumConstant]: LessConstraint[Double, V] with - override inline def test(value: Double): Boolean = value < doubleValue[V] + override inline def test(inline value: Double): Boolean = value < doubleValue[V] inline given bigDecimalDouble[V <: NumConstant]: LessConstraint[BigDecimal, V] with - override inline def test(value: BigDecimal): Boolean = value < BigDecimal(doubleValue[V]) + override inline def test(inline value: BigDecimal): Boolean = ${ checkBigDecimalDouble('value, '{ doubleValue[V] }) } inline given bigDecimalLong[V <: Int | Long]: LessConstraint[BigDecimal, V] with - override inline def test(value: BigDecimal): Boolean = value < BigDecimal(longValue[V]) + override inline def test(inline value: BigDecimal): Boolean = ${ checkBigDecimalLong('value, '{ longValue[V] }) } inline given [V <: Int | Long]: LessConstraint[BigInt, V] with - override inline def test(value: BigInt): Boolean = value < BigInt(longValue[V]) + override inline def test(inline value: BigInt): Boolean = ${ checkBigInt('value, '{ longValue[V] }) } + + private def checkBigDecimalDouble(expr: Expr[BigDecimal], thanExpr: Expr[Double])(using Quotes): Expr[Boolean] = + val rflUtil = reflectUtil + import rflUtil.* + + (expr.decode, thanExpr.decode) match + case (Right(value), Right(than)) => Expr(value < BigDecimal(than)) + case _ => '{ $expr < BigDecimal($thanExpr) } + + private def checkBigDecimalLong(expr: Expr[BigDecimal], thanExpr: Expr[Long])(using Quotes): Expr[Boolean] = + val rflUtil = reflectUtil + import rflUtil.* + + (expr.decode, thanExpr.decode) match + case (Right(value), Right(than)) => Expr(value < BigDecimal(than)) + case _ => '{ $expr < BigDecimal($thanExpr) } + + private def checkBigInt(expr: Expr[BigInt], thanExpr: Expr[Long])(using Quotes): Expr[Boolean] = + val rflUtil = reflectUtil + import rflUtil.* + + (expr.decode, thanExpr.decode) match + case (Right(value), Right(than)) => Expr(value < BigInt(than)) + case _ => '{ $expr < BigInt($thanExpr) } given [V1, V2](using V1 < V2 =:= true): (Less[V1] ==> Less[V2]) = Implication() @@ -222,24 +273,40 @@ object numeric: override inline def message: String = "Should be a multiple of " + stringValue[V] inline given [V <: NumConstant]: MultipleConstraint[Int, V] with - override inline def test(value: Int): Boolean = value % doubleValue[V] == 0 + override inline def test(inline value: Int): Boolean = value % doubleValue[V] == 0 inline given [V <: NumConstant]: MultipleConstraint[Long, V] with - override inline def test(value: Long): Boolean = value % doubleValue[V] == 0 + override inline def test(inline value: Long): Boolean = value % doubleValue[V] == 0 inline given [V <: NumConstant]: MultipleConstraint[Float, V] with - override inline def test(value: Float): Boolean = value % doubleValue[V] == 0 + override inline def test(inline value: Float): Boolean = value % doubleValue[V] == 0 inline given [V <: NumConstant]: MultipleConstraint[Double, V] with - override inline def test(value: Double): Boolean = value % doubleValue[V] == 0 + override inline def test(inline value: Double): Boolean = value % doubleValue[V] == 0 + + inline given [V <: NumConstant]: MultipleConstraint[BigDecimal, V] with + + override inline def test(inline value: BigDecimal): Boolean = ${ checkBigDecimal('value, '{ doubleValue[V] }) } inline given [V <: Int | Long]: MultipleConstraint[BigInt, V] with - override inline def test(value: BigInt): Boolean = value % BigInt(longValue[V]) == 0 + override inline def test(inline value: BigInt): Boolean = ${ checkBigInt('value, '{ longValue[V] }) } - inline given [V <: NumConstant]: MultipleConstraint[BigDecimal, V] with + private def checkBigDecimal(expr: Expr[BigDecimal], thanExpr: Expr[Double])(using Quotes): Expr[Boolean] = + val rflUtil = reflectUtil + import rflUtil.* + + (expr.decode, thanExpr.decode) match + case (Right(value), Right(than)) => Expr(value % BigDecimal(than) == 0) + case _ => '{ $expr % BigDecimal($thanExpr) == 0 } + + private def checkBigInt(expr: Expr[BigInt], thanExpr: Expr[Long])(using Quotes): Expr[Boolean] = + val rflUtil = reflectUtil + import rflUtil.* - override inline def test(value: BigDecimal): Boolean = value % BigDecimal(doubleValue[V]) == 0 + (expr.decode, thanExpr.decode) match + case (Right(value), Right(than)) => Expr(value % BigInt(than) == 0) + case _ => '{ $expr % BigInt($thanExpr) == 0 } given [A, V1 <: A, V2 <: A](using V1 % V2 =:= Zero[A]): (Multiple[V1] ==> Multiple[V2]) = Implication() @@ -248,39 +315,55 @@ object numeric: override inline def message: String = "Should divide " + stringValue[V] inline given [V <: NumConstant]: DivideConstraint[Int, V] with - override inline def test(value: Int): Boolean = doubleValue[V] % value == 0 + override inline def test(inline value: Int): Boolean = doubleValue[V] % value == 0 inline given [V <: NumConstant]: DivideConstraint[Long, V] with - override inline def test(value: Long): Boolean = doubleValue[V] % value == 0 + override inline def test(inline value: Long): Boolean = doubleValue[V] % value == 0 inline given [V <: NumConstant]: DivideConstraint[Float, V] with - override inline def test(value: Float): Boolean = doubleValue[V] % value == 0 + override inline def test(inline value: Float): Boolean = doubleValue[V] % value == 0 inline given [V <: NumConstant]: DivideConstraint[Double, V] with - override inline def test(value: Double): Boolean = doubleValue[V] % value == 0 + override inline def test(inline value: Double): Boolean = doubleValue[V] % value == 0 + + inline given [V <: NumConstant]: DivideConstraint[BigDecimal, V] with + override inline def test(inline value: BigDecimal): Boolean = ${ checkBigDecimal('value, '{ doubleValue[V] }) } inline given [V <: Int | Long]: DivideConstraint[BigInt, V] with - override inline def test(value: BigInt): Boolean = BigInt(longValue[V]) % value == 0 + override inline def test(inline value: BigInt): Boolean = ${ checkBigInt('value, '{ longValue[V] }) } - inline given [V <: NumConstant]: DivideConstraint[BigDecimal, V] with - override inline def test(value: BigDecimal): Boolean = BigDecimal(doubleValue[V]) % value == 0 + private def checkBigDecimal(expr: Expr[BigDecimal], thanExpr: Expr[Double])(using Quotes): Expr[Boolean] = + val rflUtil = reflectUtil + import rflUtil.* + + (expr.decode, thanExpr.decode) match + case (Right(value), Right(than)) => Expr(BigDecimal(than) % value == 0) + case _ => '{ BigDecimal($thanExpr) % $expr == 0 } + + private def checkBigInt(expr: Expr[BigInt], thanExpr: Expr[Long])(using Quotes): Expr[Boolean] = + val rflUtil = reflectUtil + import rflUtil.* + + (expr.decode, thanExpr.decode) match + case (Right(value), Right(than)) => Expr(BigInt(than) % value == 0) + case _ => '{ BigInt($thanExpr) % $expr == 0 } object NaN: private trait NaNConstraint[A] extends Constraint[A, NaN]: override inline def message: String = "Should be an unrepresentable number" inline given NaNConstraint[Float] with - override inline def test(value: Float): Boolean = value.isNaN + override inline def test(inline value: Float): Boolean = value.isNaN inline given NaNConstraint[Double] with - override inline def test(value: Double): Boolean = value.isNaN + override inline def test(inline value: Double): Boolean = value.isNaN object Infinity: private trait InfinityConstraint[A] extends Constraint[A, Infinity]: override inline def message: String = "Should be -infinity or +infinity" inline given InfinityConstraint[Float] with - override inline def test(value: Float): Boolean = value.isInfinity + override inline def test(inline value: Float): Boolean = value.isInfinity inline given InfinityConstraint[Double] with - override inline def test(value: Double): Boolean = value.isInfinity + override inline def test(inline value: Double): Boolean = value.isInfinity diff --git a/main/src/io/github/iltotore/iron/constraint/string.scala b/main/src/io/github/iltotore/iron/constraint/string.scala index 7a4ed2c1..fd45c30e 100644 --- a/main/src/io/github/iltotore/iron/constraint/string.scala +++ b/main/src/io/github/iltotore/iron/constraint/string.scala @@ -100,7 +100,7 @@ object string: inline given [V <: String]: Constraint[String, StartWith[V]] with - override inline def test(value: String): Boolean = ${ check('value, '{ constValue[V] }) } + override inline def test(inline value: String): Boolean = ${ check('value, '{ constValue[V] }) } override inline def message: String = "Should start with " + stringValue[V] @@ -116,7 +116,7 @@ object string: inline given [V <: String]: Constraint[String, EndWith[V]] with - override inline def test(value: String): Boolean = ${ check('value, '{ constValue[V] }) } + override inline def test(inline value: String): Boolean = ${ check('value, '{ constValue[V] }) } override inline def message: String = "Should end with " + stringValue[V] @@ -132,7 +132,7 @@ object string: inline given [V <: String]: Constraint[String, Match[V]] with - override inline def test(value: String): Boolean = ${ check('value, '{ constValue[V] }) } + override inline def test(inline value: String): Boolean = ${ check('value, '{ constValue[V] }) } override inline def message: String = "Should match " + constValue[V] diff --git a/main/src/io/github/iltotore/iron/macros/ReflectUtil.scala b/main/src/io/github/iltotore/iron/macros/ReflectUtil.scala index 3e157ac0..ae1117bc 100644 --- a/main/src/io/github/iltotore/iron/macros/ReflectUtil.scala +++ b/main/src/io/github/iltotore/iron/macros/ReflectUtil.scala @@ -1,6 +1,7 @@ package io.github.iltotore.iron.macros import scala.quoted.* +import io.github.iltotore.iron.compileTime.NumConstant /** * Low AST related utils. @@ -20,13 +21,17 @@ class ReflectUtil[Q <: Quotes & Singleton](using val _quotes: Q): import _quotes.reflect.* + type DecodingResult[+T] = Either[DecodingFailure, T] + extension [T](result: DecodingResult[T]) + private def as[U]: DecodingResult[U] = result.asInstanceOf[DecodingResult[U]] + extension [T: Type](expr: Expr[T]) /** * Decode this expression. * * @return the value of this expression found at compile time or a [[DecodingFailure]] */ - def decode: Either[DecodingFailure, T] = ExprDecoder.decodeTerm(expr.asTerm, Map.empty) + def decode: DecodingResult[T] = ExprDecoder.decodeTerm(expr.asTerm, Map.empty).as[T] /** * A decoding failure. @@ -66,9 +71,9 @@ class ReflectUtil[Q <: Quotes & Singleton](using val _quotes: Q): * * @param parameters the list of decoded parameters, whether an failure or a value of unknown type */ - case ApplyNotInlined(name: String, parameters: List[Either[DecodingFailure, ?]]) + case ApplyNotInlined(name: String, parameters: List[DecodingResult[?]]) - case VarArgsNotInlined(args: List[Either[DecodingFailure, ?]]) + case VarArgsNotInlined(args: List[DecodingResult[?]]) /** * A boolean OR is not inlined. @@ -76,7 +81,7 @@ class ReflectUtil[Q <: Quotes & Singleton](using val _quotes: Q): * @param left the left operand * @param right the right operand */ - case OrNotInlined(left: Either[DecodingFailure, Boolean], right: Either[DecodingFailure, Boolean]) + case OrNotInlined(left: DecodingResult[Boolean], right: Either[DecodingFailure, Boolean]) /** * A boolean AND is not inlined. @@ -84,14 +89,14 @@ class ReflectUtil[Q <: Quotes & Singleton](using val _quotes: Q): * @param left the left operand * @param right the right operand */ - case AndNotInlined(left: Either[DecodingFailure, Boolean], right: Either[DecodingFailure, Boolean]) + case AndNotInlined(left: DecodingResult[Boolean], right: Either[DecodingFailure, Boolean]) /** * Some part of the decoded String are not inlined. A more specialized version of [[ApplyNotInlined]]. * * @param parts the parts of the String */ - case StringPartsNotInlined(parts: List[Either[DecodingFailure, String]]) + case StringPartsNotInlined(parts: List[DecodingResult[String]]) /** * The given String interpolator cannot be inlined. @@ -180,8 +185,12 @@ class ReflectUtil[Q <: Quotes & Singleton](using val _quotes: Q): object ExprDecoder: - private val enhancedDecoders: Map[TypeRepr, (Term, Map[String, ?]) => Either[DecodingFailure, ?]] = Map( + private val enhancedDecoders: Map[TypeRepr, (Term, Map[String, ?]) => DecodingResult[?]] = Map( TypeRepr.of[Boolean] -> decodeBoolean, + TypeRepr.of[BigDecimal] -> decodeBigDecimal, + TypeRepr.of[BigInt] -> decodeBigInt, + TypeRepr.of[List[?]] -> decodeList, + TypeRepr.of[Set[?]] -> decodeSet, TypeRepr.of[String] -> decodeString ) @@ -190,19 +199,18 @@ class ReflectUtil[Q <: Quotes & Singleton](using val _quotes: Q): * * @param tree the term to decode * @param definitions the decoded definitions in scope - * @tparam T the expected type of this term used as implicit cast for convenience * @return the value of the given term found at compile time or a [[DecodingFailure]] */ - def decodeTerm[T](tree: Term, definitions: Map[String, ?]): Either[DecodingFailure, T] = + def decodeTerm(tree: Term, definitions: Map[String, ?]): DecodingResult[?] = val specializedResult = enhancedDecoders .collectFirst: - case (k, v) if k =:= tree.tpe => v + case (k, v) if tree.tpe <:< k => v .toRight(DecodingFailure.Unknown) .flatMap(_.apply(tree, definitions)) specializedResult match case Left(DecodingFailure.Unknown) => decodeUnspecializedTerm(tree, definitions) - case result => result.asInstanceOf[Either[DecodingFailure, T]] + case result => result /** * Decode a term using only unspecialized cases. @@ -212,25 +220,25 @@ class ReflectUtil[Q <: Quotes & Singleton](using val _quotes: Q): * @tparam T the expected type of this term used as implicit cast for convenience * @return the value of the given term found at compile time or a [[DecodingFailure]] */ - def decodeUnspecializedTerm[T](tree: Term, definitions: Map[String, ?]): Either[DecodingFailure, T] = + def decodeUnspecializedTerm(tree: Term, definitions: Map[String, ?]): DecodingResult[?] = tree match case block @ Block(stats, e) => if stats.isEmpty then decodeTerm(e, definitions) else Left(DecodingFailure.HasStatements(block)) case Inlined(_, bindings, e) => val (failures, values) = bindings - .map[(String, Either[DecodingFailure, ?])](b => (b.name, decodeBinding(b, definitions))) + .map[(String, DecodingResult[?])](b => (b.name, decodeBinding(b, definitions))) .partitionMap: case (name, Right(value)) => Right((name, value)) case (name, Left(failure)) => Left((name, failure)) - (failures, decodeTerm[T](e, definitions ++ values.toMap)) match + (failures, decodeTerm(e, definitions ++ values.toMap)) match case (_, Right(value)) => Right(value) case (Nil, Left(failure)) => Left(failure) case (failures, Left(_)) => Left(DecodingFailure.HasBindings(failures)) - case Apply(Select(left, "=="), List(right)) => (decodeTerm[Any](left, definitions), decodeTerm[Any](right, definitions)) match - case (Right(leftValue), Right(rightValue)) => Right((leftValue == rightValue).asInstanceOf[T]) + case Apply(Select(left, "=="), List(right)) => (decodeTerm(left, definitions), decodeTerm(right, definitions)) match + case (Right(leftValue), Right(rightValue)) => Right((leftValue == rightValue)) case (leftResult, rightResult) => Left(DecodingFailure.ApplyNotInlined("==", List(leftResult, rightResult))) case Apply(Select(leftOperand, name), operands) => @@ -253,19 +261,19 @@ class ReflectUtil[Q <: Quotes & Singleton](using val _quotes: Q): result if hasFailure then Left(DecodingFailure.VarArgsNotInlined(results)) - else Right(results.map(_.getOrElse((???): String)).asInstanceOf[T]) + else Right(results.map(_.getOrElse((???): String))) case Typed(e, _) => decodeTerm(e, definitions) - case Ident(name) => definitions - .get(name) - .toRight(DecodingFailure.NotInlined(tree)) - .asInstanceOf[Either[DecodingFailure, T]] - case _ => tree.tpe.widenTermRefByName match - case ConstantType(c) => Right(c.value.asInstanceOf[T]) - case _ => Left(DecodingFailure.NotInlined(tree)) + case ConstantType(c) => Right(c.value) + case _ => tree match + case Ident(name) => definitions + .get(name) + .toRight(DecodingFailure.NotInlined(tree)) + + case _ => Left(DecodingFailure.NotInlined(tree)) /** * Decode a binding/definition. @@ -275,7 +283,7 @@ class ReflectUtil[Q <: Quotes & Singleton](using val _quotes: Q): * @tparam T the expected type of this term used as implicit cast for convenience * @return the value of the given definition found at compile time or a [[DecodingFailure]] */ - def decodeBinding[T](definition: Definition, definitions: Map[String, ?]): Either[DecodingFailure, T] = definition match + def decodeBinding(definition: Definition, definitions: Map[String, ?]): DecodingResult[?] = definition match case ValDef(name, tpeTree, Some(term)) => decodeTerm(term, definitions) case DefDef(name, Nil, tpeTree, Some(term)) => decodeTerm(term, definitions) case _ => Left(DecodingFailure.DefinitionNotInlined(definition.name)) @@ -287,16 +295,16 @@ class ReflectUtil[Q <: Quotes & Singleton](using val _quotes: Q): * @param definitions the decoded definitions in scope * @return the value of the given term found at compile time or a [[DecodingFailure]] */ - def decodeBoolean(term: Term, definitions: Map[String, ?]): Either[DecodingFailure, Boolean] = term match + def decodeBoolean(term: Term, definitions: Map[String, ?]): DecodingResult[?] = term match case Apply(Select(left, "||"), List(right)) if left.tpe <:< TypeRepr.of[Boolean] && right.tpe <:< TypeRepr.of[Boolean] => // OR - (decodeTerm[Boolean](left, definitions), decodeTerm[Boolean](right, definitions)) match + (decodeTerm(left, definitions).as[Boolean], decodeTerm(right, definitions).as[Boolean]) match case (Right(true), _) => Right(true) case (_, Right(true)) => Right(true) case (Right(leftValue), Right(rightValue)) => Right(leftValue || rightValue) case (leftResult, rightResult) => Left(DecodingFailure.OrNotInlined(leftResult, rightResult)) case Apply(Select(left, "&&"), List(right)) if left.tpe <:< TypeRepr.of[Boolean] && right.tpe <:< TypeRepr.of[Boolean] => // AND - (decodeTerm[Boolean](left, definitions), decodeTerm[Boolean](right, definitions)) match + (decodeTerm(left, definitions).as[Boolean], decodeTerm(right, definitions).as[Boolean]) match case (Right(false), _) => Right(false) case (_, Right(false)) => Right(false) case (Right(leftValue), Right(rightValue)) => Right(leftValue && rightValue) @@ -311,9 +319,9 @@ class ReflectUtil[Q <: Quotes & Singleton](using val _quotes: Q): * @param definitions the decoded definitions in scope * @return the value of the given term found at compile time or a [[DecodingFailure]] */ - def decodeString(term: Term, definitions: Map[String, ?]): Either[DecodingFailure, String] = term match + def decodeString(term: Term, definitions: Map[String, ?]): DecodingResult[String] = term match case Apply(Select(left, "+"), List(right)) if left.tpe <:< TypeRepr.of[String] && right.tpe <:< TypeRepr.of[String] => - (decodeTerm[String](left, definitions), decodeTerm[String](right, definitions)) match + (decodeTerm(left, definitions).as[String], decodeTerm(right, definitions).as[String]) match case (Right(leftValue), Right(rightValue)) => Right(leftValue + rightValue) case (Left(DecodingFailure.StringPartsNotInlined(lparts)), Left(DecodingFailure.StringPartsNotInlined(rparts))) => Left(DecodingFailure.StringPartsNotInlined(lparts ++ rparts)) @@ -324,3 +332,63 @@ class ReflectUtil[Q <: Quotes & Singleton](using val _quotes: Q): case (leftResult, rightResult) => Left(DecodingFailure.StringPartsNotInlined(List(leftResult, rightResult))) case _ => Left(DecodingFailure.Unknown) + + /** + * Decode a [[BigInt]] term using only [[BigInt]]-specific cases. + * + * @param term the term to decode + * @param definitions the decoded definitions in scope + * @return the value of the given term found at compile time or a [[DecodingFailure]] + */ + def decodeBigInt(term: Term, definitions: Map[String, ?]): DecodingResult[BigInt] = + term match + case Apply(Select(Ident("BigInt"), "apply"), List(value)) => + decodeTerm(value, definitions).as[Int | Long].map: + case x: Int => BigInt(x) + case x: Long => BigInt(x) + case _ => Left(DecodingFailure.Unknown) + + /** + * Decode a [[BigDecimal]] term using only [[BigDecimal]]-specific cases. + * + * @param term the term to decode + * @param definitions the decoded definitions in scope + * @return the value of the given term found at compile time or a [[DecodingFailure]] + */ + def decodeBigDecimal(term: Term, definitions: Map[String, ?]): DecodingResult[BigDecimal] = + term match + case Apply(Select(Ident("BigDecimal"), "apply"), List(value)) => + decodeTerm(value, definitions).as[NumConstant].map: + case x: Int => BigDecimal(x) + case x: Long => BigDecimal(x) + case x: Float => BigDecimal(x) + case x: Double => BigDecimal(x) + + case _ => Left(DecodingFailure.Unknown) + + /** + * Decode a [[List]] term using only [[List]]-specific cases. + * + * @param term the term to decode + * @param definitions the decoded definitions in scope + * @return the value of the given term found at compile time or a [[DecodingFailure]] + */ + def decodeList(term: Term, definitions: Map[String, ?]): DecodingResult[List[?]] = + term match + case Apply(TypeApply(Select(Ident("List"), "apply"), _), List(values)) => + decodeTerm(values, definitions).as[List[?]] + case _ => Left(DecodingFailure.Unknown) + + /** + * Decode a [[Set]] term using only [[Set]]-specific cases. + * + * @param term the term to decode + * @param definitions the decoded definitions in scope + * @return the value of the given term found at compile time or a [[DecodingFailure]] + */ + def decodeSet(term: Term, definitions: Map[String, ?]): DecodingResult[Set[?]] = + term match + case Apply(TypeApply(Select(Ident("Set"), "apply"), _), List(values)) => + decodeTerm(values, definitions).as[List[?]].map(_.toSet) + case _ => Left(DecodingFailure.Unknown) + diff --git a/main/test/src/io/github/iltotore/iron/testing/CollectionSuite.scala b/main/test/src/io/github/iltotore/iron/testing/CollectionSuite.scala index b9984325..45e510fc 100644 --- a/main/test/src/io/github/iltotore/iron/testing/CollectionSuite.scala +++ b/main/test/src/io/github/iltotore/iron/testing/CollectionSuite.scala @@ -11,7 +11,7 @@ object CollectionSuite extends TestSuite: given Constraint[Char, IsA] with - override inline def test(value: Char): Boolean = value == 'a' + override inline def test(inline value: Char): Boolean = value == 'a' override inline def message: String = "Should be 'a'"