Search code examples
scalascala-macroscompile-time-constant

Is there a way to test at compile-time that a constant is a compile-time constant?


Given how difficult it is to know whether an arithmetic final val expression will be compiled to a compile-time constant, and how easy it is to accidentally break compile-time-ness...

Can anyone think of an easy way to verify, at compile-time, that the compiler has actually created a compile-time constant from, say, a complex arithmetic expression? I'm guessing this might be some kind of annotation or macro, but maybe there's something simpler. For example, maybe something like:

   @CompileTime final val HALF_INFINITY = Int.MaxValue / 2

would be possible.


Solution

  • Luckily enough, macros are wired into typechecking (in the sense that macro arguments are typechecked prior to macro expansion), and typechecking folds constants, so it looks like it should be sufficient to check for Literal(Constant(_)) in a macro to make sure that macro's argument is a constant.

    Note. Macro annotations implemented in macro paradise expand prior to typechecking of the annottees, which means that their arguments won't be constfolded during the expansion, making macro annotations a less convenient vehicle for carrying out this task.

    Here's the code written with Scala 2.11.0-M8 syntax for def macros. For 2.11.0-M7, replace the import with import scala.reflect.macros.{BlackboxContext => Context}. For 2.10.x, replace the import with import scala.reflect.macros.Context, rewrite the signature of impl to read def impl[T](c: Context)(x: c.Expr[T]) = ... and the signature of ensureConstant to read def ensureConstant[T](x: T): T = macro impl[T].

    // Macros.scala
    
    import scala.reflect.macros.blackbox._
    import scala.language.experimental.macros
    
    object Macros {
      def impl(c: Context)(x: c.Tree) = {
        import c.universe._
        x match {
          case Literal(Constant(_)) => x
          case _ => c.abort(c.enclosingPosition, "not a compile-time constant")
        }
      }
    
      def ensureConstant[T](x: T): T = macro impl
    }
    
    // Test.scala
    
    import Macros._
    
    object Test extends App {
      final val HALF_INFINITY = ensureConstant(Int.MaxValue / 2)
      final val HALF_INFINITY_PLUS_ONE = ensureConstant(HALF_INFINITY + 1)
      final val notConst = ensureConstant(scala.util.Random.nextInt())
    }
    
    00:26 ~/Projects/Master/sandbox (master)$ scalac Macros.scala && scalac Test.scala
    Test.scala:6: error: not a compile-time constant
          final val notConst = ensureConstant(scala.util.Random.nextInt())
                                             ^
    one error found