I’m trying to write (my first!) macro that checks if a string is a member of some predefined set:
def checkImpl(column: Expr[String])(using Quotes): Expr[String] =
import quotes.reflect.*
val literal = column.valueOrError
val columns = Expr(List("one", "two", "three"))
val contains = '{ ${ columns }.contains(${ column }) }
'{ if ${ contains } then ${ column } else ${ report.errorAndAbort("No such column") } }
And this macro fails unconditionally, i.e. else-branch is always invoked (works fine if I return another string). I’m wondering how do I rewrite it to fail compilation only when it needed.
This should help - i.e. you can do all the evaluation at compile time
def checkImpl(column: Expr[String])(using Quotes): Expr[Unit] =
import quotes.reflect.*
val literal = column.valueOrError
val columns = List("one", "two", "three")
val contains = columns.contains(literal)
if contains then '{}
else report.errorAndAbort("No such column")
what your original post tries to do is splice the result of aborting macro expansion - all splices will get evaluated at compile time, whereas the quoted if will not be evaluated until runtime
Thanks @bishabosha! Although I think I poorly formulated what I want to achieve. In fact my columns are coming from a type class instance, so assuming I’m stuck with Expr[List[String]] at the beginning.
I solved the problem I had with errorAndAbort via the inline if at the top-level function. Something like:
inline def inspectGet[T](inline col: String): String =
inline if inspect(col) then col else error // report.errorAndAbort underneath
…then I was struggling to inline that if, given that inspect actually evaluates cols.contains(col) and apparently what inline needs is something concrete as '{ true } or '{ false } (now seems stupidly obvious, but wasn’t at all when I started writing it).
Currently I’m still trying to wrap my head around phase consistency principle, how to share data between quoted and spliced blocks and what different inlinings mean.
Currently I’m stuck with following snippet:
// Members[T] is a type class instance, containing list of fields
inline def inspectGet[T](inline col: String)(using inline m: Members[T]): String =
inline if inspect(col, m) then col else error // error is a wrapper around report.errorAndAbort
private inline def inspect[T](inline col: String, inline m: Members[T]): Boolean =
${ inspectImpl('col, 'm) }
private def inspectImpl[T: Type](column: Expr[String], columns: Expr[Members[T]])(using Quotes): Expr[Boolean] =
import quotes.reflect.*
'{ ${ columns }.get.contains(${ column }) } // Feels I need to splice it somewhow to make the inline work
So it seems you want to still reduce the columns list at compiletime - so in that case you will need to be able to extract the a List[String] from the Expr[Members[T]], and at this point it would be quite difficult. I would suggest converting your representation to a Tuple type of literal string types, e.g. ("foo", "bar") - then you can try inline match with compiletime.erasedValue[T]
Thanks! So, I ended up synthesizing a Mirror inside macro with copy-paste from shapeless.
I think it’s a shame though I cannot inline a type-class instance that is synthesized at compile-time (at least I think so - inline keyword is everywhere) to a macro.
The thing is that although ("one", "two", "three"): ("one", "two", "three") is a compile-time value but List("one", "two", "three"): List[String] is a runtime value. As soon as you convert the tuple into a list, columns.contains(column) can no longer be checked at compile time.
Inlining not always means execution at compile time. In the inline method
In '{ ${ columns }.get.contains(${ column }) }, contains will be executed at runtime. But in inline if inspect(col, m) then ... you’re trying to execute inspect (i.e. contains) at compile time.
import scala.deriving.Mirror
import scala.compiletime.constValue
def inspectGet[T] = new PartiallyAppliedInspectGet[T]
class PartiallyAppliedInspectGet[T]:
inline def apply[S <: String with Singleton](inline col: S)(using inline m: Mirror.ProductOf[T]): String =
inline if constValue[Contains[S, m.MirroredElemLabels]] then col else error
type Contains[A, Tup <: Tuple] <: Boolean = Tup match
case EmptyTuple => false
case A *: _ => true
case h *: t => Contains[A, t]
Or just
inline def inspectGet[T](inline col: String)(using inline m: Mirror.ProductOf[T]): String =
inline if constValue[Contains[col.type, m.MirroredElemLabels]] then col else error
An implementation similar to your approach is
import scala.quoted.*
import scala.deriving.Mirror
inline def inspectGet[T](inline col: String)(using inline m: Mirror.ProductOf[T]): String =
inline if inspect[m.MirroredElemLabels](col) then col else error
private inline def inspect[Tup <: Tuple](inline col: String): Boolean =
${ inspectImpl[Tup]('col) }
private def inspectImpl[Tup <: Tuple: Type](column: Expr[String])(using Quotes): Expr[Boolean] =
import quotes.reflect.*
// scala.compiletime.constValueTuple[Tup] can't be used in a macro implementation because it's not inline
def constVals[T <: Tuple : Type]: List[String] = Type.of[T] match
case '[h *: t] => TypeRepr.of[h] match
case ConstantType(StringConstant(s)) => s :: constVals[t]
case '[EmptyTuple] => Nil
Expr(constVals[Tup].contains(column.valueOrAbort))
inline def inspectGet[T](inline col: String)(using inline m: Mirror.ProductOf[T]): String =
inline if constValue[Contains[col.type, m.MirroredElemLabels]] then col else error
That’s quite cool
Learning macros reminds me learning monads. When you understand something - it looks completely obvious, but before that you’re completely baffled. Basically I’m learning how to write code once again.
Also there aren’t that many resources on metaprogramming out there. But the ones that helped me a lot were (in case someone is in the same situation):