How to pass a literal Int to a macro?

I’m playing with dotty macros and want to do a kind of crappy loop unrolling. So given an expression, run it N times by repeating the expression rather than using a loop. I’m very close.

object RunN {
  inline def runN(inline n: Int)(inline arg: Unit): Unit = 
    ${runNImpl(10, 'arg)}
  
  def runNImpl(n: Int, f: Expr[Unit])(using Quotes): Expr[Unit] = {
    if (n == 0) {
      '{()}
    } else {
      '{
        ${f}
        ${runNImpl(n-1, f)}
       }
    }
  }
}

The problem is that if I replace 10 with n I get the error

Malformed macro parameter

Parameters may only be:
 * Quoted parameters or fields
 * Literal values of primitive types

Well, I do want a literal of a primitive, so that works. But how do I tell it that n is a literal of Int?

n is not a literal value. Or did I misunderstood what you mean? How do you want this to work?

I have the hardest time figuring out which parts of an inline function happen at compile time and which happen at runtime too.

You can get the constant value from some expression with .valueOrError, but I’m not sure how that will help you, or how to get a quoted parameter from a normal parameter, and what their difference is exactly, or how to get an expression from a quoted paramter.

Well, from the error message, it seems I want n to be a literal Int, but it doesn’t tell me how to do that. I’m hoping someone can tell me because I’m having a heck of a time figuring it out on my own.

At compile time, I need n to be known, and then runNImpl will generate code that repeats f n times.

Thanks for the tip about valueOrError! I was able to create what I wanted.

object RunN {
  inline def runN(inline n: Int)(inline arg: Unit): Unit = 
    ${runNImpl('n, 'arg)}
  
  def runNImpl(n:  Expr[Int], f: Expr[Unit])(using quotes: Quotes): Expr[Unit] = {
    if (n.valueOrError == 0) {
      '{()}
    } else {
      '{
        ${f}
        ${runNImpl('{${n} - 1}, f)}
      }
    }
  }
}

The compiler seems to be reasonably good at accepting expressions for n. I was pleasantly surprised that it could use final vals in the expression.

Yes, that works, but then you have to take an Expr[Int] rather than an inline n: Int

Is that bad? I’m open to alternatives.

I don’t know. Maybe it’s fine.

I suppose this doesn’t answer the question directly, but an alternative to a macro might be inline match.

inline def runN(inline n: Int)(inline arg: Unit): Unit = 
    inline n match {
      case 0 => ()
      case i if i > 0 =>
        arg
        runN(i - 1)(arg)
      case i if i < 0 => compiletime.error(compiletime.codeOf(n) + " is negative")
      case _ => compiletime.error(compiletime.codeOf(n) + " is not a literal")
    }

Runnable Scastie

3 Likes