Eager evaluation of `report.errorAndAbort`

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")
1 Like

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

1 Like

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]

1 Like

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

inline def foo: Unit = println("test")

or in the macro

inline def foo: Unit = ${fooImpl}

def fooImpl(using Quotes): Expr[Unit] =
  '{println("test")}

println is executed at runtime while in the macro

inline def foo: Unit = ${fooImpl}

def fooImpl(using Quotes): Expr[Unit] =
  println("test")
  '{}

println is executed at compile time.

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.

Maybe the easiest implementation is

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))
1 Like
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 :point_up:

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):

3 Likes