diff --git a/main/src/io/github/iltotore/iron/constraint/char.scala b/main/src/io/github/iltotore/iron/constraint/char.scala index 0d07324e..e88a47ec 100644 --- a/main/src/io/github/iltotore/iron/constraint/char.scala +++ b/main/src/io/github/iltotore/iron/constraint/char.scala @@ -3,6 +3,7 @@ package io.github.iltotore.iron.constraint import io.github.iltotore.iron.Constraint import io.github.iltotore.iron.compileTime.* import io.github.iltotore.iron.constraint.any.Not +import io.github.iltotore.iron.macros.reflectUtil import scala.quoted.* @@ -47,9 +48,12 @@ object char: override inline def message: String = "Should be a whitespace" private def check(expr: Expr[Char])(using Quotes): Expr[Boolean] = - expr.value match - case Some(value) => Expr(value.isWhitespace) - case None => '{ $expr.isWhitespace } + val rflUtil = reflectUtil + import rflUtil.* + + expr.decode match + case Right(value) => Expr(value.isWhitespace) + case _ => '{ $expr.isWhitespace } object LowerCase: @@ -60,9 +64,12 @@ object char: override inline def message: String = "Should be a lower cased" private def check(expr: Expr[Char])(using Quotes): Expr[Boolean] = - expr.value match - case Some(value) => Expr(value.isLower) - case None => '{ $expr.isLower } + val rflUtil = reflectUtil + import rflUtil.* + + expr.decode match + case Right(value) => Expr(value.isLower) + case _ => '{ $expr.isLower } object UpperCase: @@ -73,9 +80,12 @@ object char: override inline def message: String = "Should be a upper cased" private def check(expr: Expr[Char])(using Quotes): Expr[Boolean] = - expr.value match - case Some(value) => Expr(value.isUpper) - case None => '{ $expr.isUpper } + val rflUtil = reflectUtil + import rflUtil.* + + expr.decode match + case Right(value) => Expr(value.isUpper) + case _ => '{ $expr.isUpper } object Digit: @@ -86,9 +96,12 @@ object char: override inline def message: String = "Should be a digit" private def check(expr: Expr[Char])(using Quotes): Expr[Boolean] = - expr.value match - case Some(value) => Expr(value.isDigit) - case None => '{ $expr.isDigit } + val rflUtil = reflectUtil + import rflUtil.* + + expr.decode match + case Right(value) => Expr(value.isDigit) + case _ => '{ $expr.isDigit } object Letter: @@ -99,6 +112,9 @@ object char: override inline def message: String = "Should be a letter" private def check(expr: Expr[Char])(using Quotes): Expr[Boolean] = - expr.value match - case Some(value) => Expr(value.isLetter) - case None => '{ $expr.isLetter } + val rflUtil = reflectUtil + import rflUtil.* + + expr.decode match + case Right(value) => Expr(value.isLetter) + case _ => '{ $expr.isLetter } diff --git a/main/src/io/github/iltotore/iron/constraint/collection.scala b/main/src/io/github/iltotore/iron/constraint/collection.scala index 3b9d2ab4..22b8cd4f 100644 --- a/main/src/io/github/iltotore/iron/constraint/collection.scala +++ b/main/src/io/github/iltotore/iron/constraint/collection.scala @@ -4,6 +4,7 @@ import io.github.iltotore.iron.{:|, ==>, Constraint, Implication} import io.github.iltotore.iron.compileTime.* import io.github.iltotore.iron.constraint.any.{DescribedAs, StrictEqual} import io.github.iltotore.iron.constraint.numeric.{GreaterEqual, LessEqual} +import io.github.iltotore.iron.macros.reflectUtil import scala.compiletime.{constValue, summonInline} import scala.compiletime.ops.string.Length @@ -111,13 +112,12 @@ object collection: inline given lengthString[C, Impl <: Constraint[Int, C]](using inline impl: Impl): LengthString[C, Impl] = new LengthString private def checkString[C, Impl <: Constraint[Int, C]](expr: Expr[String], constraintExpr: Expr[Impl])(using Quotes): Expr[Boolean] = + val rflUtil = reflectUtil + import rflUtil.* - import quotes.reflect.* - - expr.value match - case Some(value) => applyConstraint(Expr(value.length), constraintExpr) - - case None => applyConstraint('{ $expr.length }, constraintExpr) + expr.decode match + case Right(value) => applyConstraint(Expr(value.length), constraintExpr) + case _ => applyConstraint('{ $expr.length }, constraintExpr) given [C1, C2](using C1 ==> C2): (Length[C1] ==> Length[C2]) = Implication() @@ -135,8 +135,11 @@ object collection: override inline def message: String = "Should contain the string " + constValue[V] private def checkString(expr: Expr[String], partExpr: Expr[String])(using Quotes): Expr[Boolean] = - (expr.value, partExpr.value) match - case (Some(value), Some(part)) => Expr(value.contains(part)) + val rflUtil = reflectUtil + import rflUtil.* + + (expr.decode, partExpr.decode) match + case (Right(value), Right(part)) => Expr(value.contains(part)) case _ => '{ ${ expr }.contains($partExpr) } object ForAll: @@ -159,17 +162,17 @@ object collection: inline given forAllString[C, Impl <: Constraint[Char, C]](using inline impl: Impl): ForAllString[C, Impl] = new ForAllString private def checkString[C, Impl <: Constraint[Char, C]](expr: Expr[String], constraintExpr: Expr[Impl])(using Quotes): Expr[Boolean] = + val rflUtil = reflectUtil + import rflUtil.* - import quotes.reflect.* - - expr.value match - case Some(value) => + expr.decode match + case Right(value) => value .map(Expr.apply) .map(applyConstraint(_, constraintExpr)) .foldLeft(Expr(true))((e, t) => '{ $e && $t }) - case None => '{ $expr.forallOptimized(c => ${ applyConstraint('c, constraintExpr) }) } + case _ => '{ $expr.forallOptimized(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() @@ -196,18 +199,18 @@ object collection: inline given initString[C, Impl <: Constraint[Char, C]](using inline impl: Impl): InitString[C, Impl] = new InitString private def checkString[C, Impl <: Constraint[Char, C]](expr: Expr[String], constraintExpr: Expr[Impl])(using Quotes): Expr[Boolean] = + val rflUtil = reflectUtil + import rflUtil.* - import quotes.reflect.* - - expr.value match - case Some(value) => + expr.decode match + case Right(value) => value .init .map(Expr.apply) .map(applyConstraint(_, constraintExpr)) .foldLeft(Expr(true))((e, t) => '{ $e && $t }) - case None => '{ $expr.init.forallOptimized(c => ${ applyConstraint('c, constraintExpr) }) } + case _ => '{ $expr.init.forallOptimized(c => ${ applyConstraint('c, constraintExpr) }) } given [C1, C2](using C1 ==> C2): (Init[C1] ==> Exists[C2]) = Implication() @@ -233,18 +236,18 @@ object collection: inline given tailString[C, Impl <: Constraint[Char, C]](using inline impl: Impl): TailString[C, Impl] = new TailString private def checkString[C, Impl <: Constraint[Char, C]](expr: Expr[String], constraintExpr: Expr[Impl])(using Quotes): Expr[Boolean] = + val rflUtil = reflectUtil + import rflUtil.* - import quotes.reflect.* - - expr.value match - case Some(value) => + expr.decode match + case Right(value) => value .tail .map(Expr.apply) .map(applyConstraint(_, constraintExpr)) .foldLeft(Expr(true))((e, t) => '{ $e && $t }) - case None => '{ $expr.tail.forallOptimized(c => ${ applyConstraint('c, constraintExpr) }) } + case _ => '{ $expr.tail.forallOptimized(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() @@ -269,17 +272,17 @@ object collection: inline given existsString[C, Impl <: Constraint[Char, C]](using inline impl: Impl): ExistsString[C, Impl] = new ExistsString private def checkString[C, Impl <: Constraint[Char, C]](expr: Expr[String], constraintExpr: Expr[Impl])(using Quotes): Expr[Boolean] = + val rflUtil = reflectUtil + import rflUtil.* - import quotes.reflect.* - - expr.value match - case Some(value) => + expr.decode match + case Right(value) => value .map(Expr.apply) .map(applyConstraint(_, constraintExpr)) .foldLeft(Expr(false))((e, t) => '{ $e || $t }) - case None => '{ $expr.existsOptimized(c => ${ applyConstraint('c, constraintExpr) }) } + case _ => '{ $expr.existsOptimized(c => ${ applyConstraint('c, constraintExpr) }) } object Head: @@ -301,12 +304,15 @@ object collection: inline given headString[C, Impl <: Constraint[Char, C]](using inline impl: Impl): HeadString[C, Impl] = new HeadString private def checkString[C, Impl <: Constraint[Char, C]](expr: Expr[String], constraintExpr: Expr[Impl])(using Quotes): Expr[Boolean] = - expr.value match - case Some(value) => + val rflUtil = reflectUtil + import rflUtil.* + + expr.decode match + case Right(value) => value.headOption match case Some(head) => applyConstraint(Expr(head), constraintExpr) case None => Expr(false) - case None => '{ $expr.headOption.exists(head => ${ applyConstraint('{ head }, constraintExpr) }) } + case _ => '{ $expr.headOption.exists(head => ${ applyConstraint('{ head }, constraintExpr) }) } given [C1, C2](using C1 ==> C2): (Head[C1] ==> Exists[C2]) = Implication() @@ -330,12 +336,15 @@ object collection: inline given lastString[C, Impl <: Constraint[Char, C]](using inline impl: Impl): LastString[C, Impl] = new LastString private def checkString[C, Impl <: Constraint[Char, C]](expr: Expr[String], constraintExpr: Expr[Impl])(using Quotes): Expr[Boolean] = - expr.value match - case Some(value) => + val rflUtil = reflectUtil + import rflUtil.* + + expr.decode match + case Right(value) => value.lastOption match case Some(last) => applyConstraint(Expr(last), constraintExpr) case None => Expr(false) - case None => '{ $expr.lastOption.exists(last => ${ applyConstraint('{ last }, constraintExpr) }) } + case _ => '{ $expr.lastOption.exists(last => ${ applyConstraint('{ last }, constraintExpr) }) } given [C1, C2](using C1 ==> C2): (Last[C1] ==> Exists[C2]) = Implication() diff --git a/main/src/io/github/iltotore/iron/constraint/string.scala b/main/src/io/github/iltotore/iron/constraint/string.scala index ab8e9955..6b6481aa 100644 --- a/main/src/io/github/iltotore/iron/constraint/string.scala +++ b/main/src/io/github/iltotore/iron/constraint/string.scala @@ -5,6 +5,7 @@ import io.github.iltotore.iron.constraint.any.* import io.github.iltotore.iron.constraint.collection.* import io.github.iltotore.iron.compileTime.* import io.github.iltotore.iron.constraint.char.{Digit, Letter, LowerCase, UpperCase, Whitespace} +import io.github.iltotore.iron.macros.reflectUtil import scala.compiletime.constValue import scala.quoted.* @@ -100,8 +101,11 @@ object string: override inline def message: String = "Should start with " + stringValue[V] private def check(expr: Expr[String], prefixExpr: Expr[String])(using Quotes): Expr[Boolean] = - (expr.value, prefixExpr.value) match - case (Some(value), Some(prefix)) => Expr(value.startsWith(prefix)) + val rflUtil = reflectUtil + import rflUtil.* + + (expr.decode, prefixExpr.decode) match + case (Right(value), Right(prefix)) => Expr(value.startsWith(prefix)) case _ => '{ $expr.startsWith($prefixExpr) } object EndWith: @@ -113,8 +117,11 @@ object string: override inline def message: String = "Should end with " + stringValue[V] private def check(expr: Expr[String], prefixExpr: Expr[String])(using Quotes): Expr[Boolean] = - (expr.value, prefixExpr.value) match - case (Some(value), Some(prefix)) => Expr(value.endsWith(prefix)) + val rflUtil = reflectUtil + import rflUtil.* + + (expr.decode, prefixExpr.decode) match + case (Right(value), Right(prefix)) => Expr(value.endsWith(prefix)) case _ => '{ $expr.endsWith($prefixExpr) } object Match: @@ -126,6 +133,9 @@ object string: override inline def message: String = "Should match " + constValue[V] private def check(valueExpr: Expr[String], regexExpr: Expr[String])(using Quotes): Expr[Boolean] = - (valueExpr.value, regexExpr.value) match - case (Some(value), Some(regex)) => Expr(value.matches(regex)) + val rflUtil = reflectUtil + import rflUtil.* + + (valueExpr.decode, regexExpr.decode) match + case (Right(value), Right(regex)) => Expr(value.matches(regex)) case _ => '{ $valueExpr.matches($regexExpr) } diff --git a/main/src/io/github/iltotore/iron/internal/package.scala b/main/src/io/github/iltotore/iron/internal/package.scala new file mode 100644 index 00000000..a1ddcd0e --- /dev/null +++ b/main/src/io/github/iltotore/iron/internal/package.scala @@ -0,0 +1,5 @@ +package io.github.iltotore.iron.internal + +extension (text: String) + + def colorized(color: String): String = s"$color$text${Console.RESET}" \ No newline at end of file diff --git a/main/src/io/github/iltotore/iron/macros/ReflectUtil.scala b/main/src/io/github/iltotore/iron/macros/ReflectUtil.scala new file mode 100644 index 00000000..eead52e5 --- /dev/null +++ b/main/src/io/github/iltotore/iron/macros/ReflectUtil.scala @@ -0,0 +1,302 @@ +package io.github.iltotore.iron.macros + +import io.github.iltotore.iron.compileTime.NumConstant + +import scala.quoted.* + +/** + * Low AST related utils. + * + * @param q the metaprogramming information + * @tparam Q the type of `_quotes` to ensure the path is valid to import. + */ +transparent inline def reflectUtil[Q <: Quotes & Singleton](using inline q: Q): ReflectUtil[Q] = new ReflectUtil[Q] + +/** + * Low AST related utils. + * + * @param _quotes the metaprogramming information + * @tparam Q the type of `_quotes` to ensure the path is valid to import. + */ +class ReflectUtil[Q <: Quotes & Singleton](using val _quotes: Q): + + import _quotes.reflect.* + + /** + * A decoding failure. + */ + enum DecodingFailure: + + /** + * A term is not inlined. Note that an `inline` val/def can still not be inlined by the compiler in some cases. + * + * @param term the term that is not inlined + */ + case NotInlined(term: Term) + + /** + * A definition is not inlined. + * + * @param name the name definition + */ + case DefinitionNotInlined(name: String) + + /** + * The term could not be fully inlined because it has runtime bindings/depends on runtime definitions. + * + * @param defFailures the definitions that the decoder failed to evaluate at compile-time + */ + case HasBindings(defFailures: List[(String, DecodingFailure)]) + + /** + * The block has possibly side-effecting statements. + * + * @param block the block containing statements + */ + case HasStatements(block: Block) + + /** + * A method application is not inlined, probably due to some parameters not being inlined. + * + * @param parameters the list of decoded parameters, whether an failure or a value of unknown type + */ + case ApplyNotInlined(name: String, parameters: List[Either[DecodingFailure, ?]]) + + /** + * A boolean OR is not inlined. + * + * @param left the left operand + * @param right the right operand + */ + case OrNotInlined(left: Either[DecodingFailure, Boolean], right: Either[DecodingFailure, Boolean]) + + /** + * A boolean AND is not inlined. + * + * @param left the left operand + * @param right the right operand + */ + case AndNotInlined(left: Either[DecodingFailure, 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]]) + + /** + * The given String interpolator cannot be inlined. + */ + case InterpolatorNotInlined(name: String) + + /** + * Pretty print this failure. + * + * @param bodyIdent the identation of the 2nd+ lines + * @param firstLineIdent the identation of the first line + * @return a pretty-formatted [[String]] representation of this failure + */ + def prettyPrint(bodyIdent: Int = 0, firstLineIdent: Int = 0): String = + val unindented = this match + case NotInlined(term) => s"Term not inlined: ${term.show}" + case DefinitionNotInlined(name) => s"Definition not inlined: $name. Only vals and zero-arg def can be inlined." + case HasBindings(defFailures) => + val failures = defFailures + .map((n, b) => s"- $n:\n${b.prettyPrint(2, 2)}") + .mkString("\n") + + s"Term depends on runtime definitions:\n$failures" + case HasStatements(block) => s"Block has statements: ${block.show}" + case ApplyNotInlined(name, operands) => + val errors = operands + .zipWithIndex + .collect: + case (Left(failure), i) => s"Arg $i:\n${failure.prettyPrint(2, 2)}" + .mkString("\n\n") + + s"Some arguments of `$name` are not inlined:\n$errors" + + case OrNotInlined(left, right) => + s"""Non-inlined boolean or. The following patterns are evaluable at compile-time: + |- || + |- || true + |- true || + | + |Left member: + |${left.fold(_.prettyPrint(2, 2), _.toString)} + | + |Right member: + |${right.fold(_.prettyPrint(2, 2), _.toString)}""".stripMargin + + case AndNotInlined(left, right) => + s"""Non-inlined boolean or. The following patterns are evaluable at compile-time: + |- || + |- || true + |- true || + | + |Left member: + |${left.fold(_.prettyPrint(2, 2), _.toString)} + | + |Right member: + |${right.fold(_.prettyPrint(2, 2), _.toString)}""".stripMargin + + case StringPartsNotInlined(parts) => + val errors = parts + .zipWithIndex + .collect: + case (Left(failure), i) => s"Arg $i:\n${failure.prettyPrint(2, 2)}" + .mkString("\n\n") + + s"String contatenation as non inlined arguments:\n$errors" + + case InterpolatorNotInlined(name) => s"This interpolator is not supported: $name. Only `s` and `raw` are supported." + + " " * firstLineIdent + unindented.replaceAll("(\r\n|\n|\r)", "$1" + " " * bodyIdent) + + override def toString: String = prettyPrint() + + /** + * A compile-time [[Expr]] decoder. Like [[FromExpr]] with more fine-grained errors. + * + * @tparam T the type of the expression to decodeExpr + */ + trait ExprDecoder[T]: + + /** + * Decode the given expression. + * + * @param expr the expression to decodeExpr + * @return the value decoded from [[expr]] or a [[DecodingFailure]] instead + */ + def decodeExpr(expr: Expr[T]): Either[DecodingFailure, T] + + extension [T](expr: Expr[T]) + + def decode(using decoder: ExprDecoder[T]): Either[DecodingFailure, T] = decoder.decodeExpr(expr) + + object ExprDecoder: + + /** + * Fallback expression decoder instance using Dotty's [[FromExpr]]. Fails with a [[DecodingFailure.NotInlined]] if the + * underlying [[FromExpr]] returns [[None]]. + */ + given [T](using fromExpr: FromExpr[T]): ExprDecoder[T] with + + override def decodeExpr(expr: Expr[T]): Either[DecodingFailure, T] = + fromExpr.unapply(expr).toRight(DecodingFailure.NotInlined(expr.asTerm)) + + private class PrimitiveExprDecoder[T <: NumConstant | Byte | Short | Boolean | String : Type] extends ExprDecoder[T]: + + private def decodeBinding(definition: Definition): Either[DecodingFailure, T] = definition match + case ValDef(name, tpeTree, Some(term)) if tpeTree.tpe <:< TypeRepr.of[T] => decodeTerm(term) + case DefDef(name, Nil, tpeTree, Some(term)) if tpeTree.tpe <:< TypeRepr.of[T] => decodeTerm(term) + case _ => Left(DecodingFailure.DefinitionNotInlined(definition.name)) + + def decodeTerm(tree: Term): Either[DecodingFailure, T] = tree match + case block@Block(stats, e) => if stats.isEmpty then decodeTerm(e) else Left(DecodingFailure.HasStatements(block)) + + case Inlined(_, bindings, e) => + val failures = + for + binding <- bindings + failure <- decodeBinding(binding).left.toOption + yield + (binding.name, failure) + + if failures.isEmpty then decodeTerm(e) + else Left(DecodingFailure.HasBindings(failures)) + + case Typed(e, _) => decodeTerm(e) + case Apply(Select(leftOperand, name), operands) => + val rightResults = operands.map(decodeTerm) + + val allResults = decodeTerm(leftOperand) match + case Left(DecodingFailure.ApplyNotInlined(n, leftResults)) if n == name => + leftResults ++ rightResults + case leftResult => + leftResult +: rightResults + + Left(DecodingFailure.ApplyNotInlined(name, allResults)) + + case _ => + tree.tpe.widenTermRefByName match + case ConstantType(c) => Right(c.value.asInstanceOf[T]) + case _ => Left(DecodingFailure.NotInlined(tree)) + + override def decodeExpr(expr: Expr[T]): Either[DecodingFailure, T] = + decodeTerm(expr.asTerm) + + /** + * Decoder for all primitives except for [[String]] and [[Boolean]] which benefit from some enhancements. + * + * @tparam T the type of the expression to decodeExpr + */ + given [T <: NumConstant | Byte | Short : Type]: ExprDecoder[T] = new PrimitiveExprDecoder[T] + + /** + * A boolean [[ExprDecoder]] that can extract value from partially inlined || and + * && operations. + * + * {{{ + * inline val x = true + * val y: Boolean = ??? + * + * x || y //inlined to `true` + * y || x //inlined to `true` + * + * inline val a = false + * val b: Boolean = ??? + * + * a && b //inlined to `false` + * b && a //inlined to `false` + * }}} + */ + given ExprDecoder[Boolean] = new PrimitiveExprDecoder[Boolean]: + + override def decodeTerm(tree: Term): Either[DecodingFailure, Boolean] = tree match + case Apply(Select(left, "||"), List(right)) if left.tpe <:< TypeRepr.of[Boolean] && right.tpe <:< TypeRepr.of[Boolean] => // OR + (decodeTerm(left), decodeTerm(right)) 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(left), decodeTerm(right)) match + case (Right(false), _) => Right(false) + case (_, Right(false)) => Right(false) + case (Right(leftValue), Right(rightValue)) => Right(leftValue && rightValue) + case (leftResult, rightResult) => Left(DecodingFailure.AndNotInlined(leftResult, rightResult)) + + case _ => super.decodeTerm(tree) + + /** + * A String [[ExprDecoder]] that can extract value from concatenated strings if all + * arguments are compile-time-extractable strings. + * + * {{{ + * inline val x = "a" + * inline val y = "b" + * val z = "c" + * + * x + y //"ab" + * x + z //DecodingFailure + * z + x //DecodingFailure + * }}} + */ + given ExprDecoder[String] = new PrimitiveExprDecoder[String]: + + override def decodeTerm(tree: Term): Either[DecodingFailure, String] = tree match + case Apply(Select(left, "+"), List(right)) if left.tpe <:< TypeRepr.of[String] && right.tpe <:< TypeRepr.of[String] => + (decodeTerm(left), decodeTerm(right)) match + case (Right(leftValue), Right(rightValue)) => Right(leftValue + rightValue) + case (Left(DecodingFailure.StringPartsNotInlined(lparts)), Left(DecodingFailure.StringPartsNotInlined(rparts))) => + Left(DecodingFailure.StringPartsNotInlined(lparts ++ rparts)) + case (Left(DecodingFailure.StringPartsNotInlined(lparts)), rightResult) => + Left(DecodingFailure.StringPartsNotInlined(lparts :+ rightResult)) + case (leftResult, Left(DecodingFailure.StringPartsNotInlined(rparts))) => + Left(DecodingFailure.StringPartsNotInlined(leftResult +: rparts)) + case (leftResult, rightResult) => Left(DecodingFailure.StringPartsNotInlined(List(leftResult, rightResult))) + + case _ => super.decodeTerm(tree) \ No newline at end of file diff --git a/main/src/io/github/iltotore/iron/macros/package.scala b/main/src/io/github/iltotore/iron/macros/package.scala index 5d44cfda..298d7f5b 100644 --- a/main/src/io/github/iltotore/iron/macros/package.scala +++ b/main/src/io/github/iltotore/iron/macros/package.scala @@ -3,96 +3,6 @@ package io.github.iltotore.iron.macros import scala.Console.{MAGENTA, RESET} import scala.quoted.* -/** - * Internal macros. - * @see [[compileTime]] for public compile-time utilities - */ - -/** - * A FromExpr[Boolean] that can extract value from partially inlined || and - * && operations. - * - * {{{ - * inline val x = true - * val y: Boolean = ??? - * - * x || y //inlined to `true` - * y || x //inlined to `true` - * - * inline val a = false - * val b: Boolean = ??? - * - * a && b //inlined to `false` - * b && a //inlined to `false` - * }}} - */ -given FromExpr[Boolean] with - - override def unapply(expr: Expr[Boolean])(using Quotes): Option[Boolean] = - - import quotes.reflect.* - - def rec(tree: Term): Option[Boolean] = - tree match - case Block(stats, e) => if stats.isEmpty then rec(e) else None - case Inlined(_, bindings, e) => - if bindings.isEmpty then rec(e) else None - case Typed(e, _) => rec(e) - case Apply(Select(left, "||"), List(right)) - if left.tpe <:< TypeRepr.of[Boolean] && right.tpe <:< TypeRepr - .of[Boolean] => // OR - rec(left) match - case Some(value) => if value then Some(true) else rec(right) - case None => rec(right).filter(x => x) - case Apply(Select(left, "&&"), List(right)) - if left.tpe <:< TypeRepr.of[Boolean] && right.tpe <:< TypeRepr - .of[Boolean] => // AND - rec(left) match - case Some(value) => if value then rec(right) else Some(false) - case None => rec(right).filterNot(x => x) - case _ => - tree.tpe.widenTermRefByName match - case ConstantType(c) => Some(c.value.asInstanceOf[Boolean]) - case _ => None - - rec(expr.asTerm) - -/** - * A FromExpr[String] that can extract value from concatenated strings if all - * arguments are compile-time-extractable strings. - * - * {{{ - * inline val x = "a" - * inline val y = "b" - * val z = "c" - * - * x + y //"ab" - * x + z //None - * z + x //None - * }}} - */ -given FromExpr[String] with - - def unapply(expr: Expr[String])(using Quotes) = - - import quotes.reflect.* - - def rec(tree: Term): Option[String] = tree match - case Block(stats, e) => if stats.isEmpty then rec(e) else None - case Inlined(_, bindings, e) => - if bindings.isEmpty then rec(e) else None - case Typed(e, _) => rec(e) - case Apply(Select(left, "+"), List(right)) - if left.tpe <:< TypeRepr.of[String] && right.tpe <:< TypeRepr - .of[String] => - rec(left).zip(rec(right)).map(_ + _) - case _ => - tree.tpe.widenTermRefByName match - case ConstantType(c) => Some(c.value.asInstanceOf[String]) - case _ => None - - rec(expr.asTerm) - /** * Asserts at compile time if the given condition is true. * @@ -107,13 +17,15 @@ inline def assertCondition[A](inline input: A, inline cond: Boolean, inline mess private def assertConditionImpl[A: Type](input: Expr[A], cond: Expr[Boolean], message: Expr[String])(using Quotes): Expr[Unit] = import quotes.reflect.* + val rflUtil = reflectUtil(using quotes) + import rflUtil.* val inputType = TypeRepr.of[A] - val messageValue = message.value.getOrElse("") - val condValue = cond.value - .getOrElse( - compileTimeError( + val messageValue = message.decode.getOrElse("") + val condValue = cond.decode + .fold( + err => compileTimeError( s"""Cannot refine value at compile-time because the predicate cannot be evaluated. |This is likely because the condition or the input value isn't fully inlined. | @@ -121,8 +33,10 @@ private def assertConditionImpl[A: Type](input: Expr[A], cond: Expr[Boolean], me | |${MAGENTA}Inlined input$RESET: ${input.show} |${MAGENTA}Inlined condition$RESET: ${cond.show} - |${MAGENTA}Message$RESET: $messageValue""".stripMargin - ) + |${MAGENTA}Message$RESET: $messageValue + |${MAGENTA}Reason$RESET: $err""".stripMargin + ), + identity ) if !condValue then