Is there a way to test at compile-time that a constant is a compile-time constant?
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
I guess it's impossible even with macros:
import scala.reflect.macros.Context
import scala.language.experimental.macros
def showMacroImpl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
import c.universe._
val inputs = annottees.map(_.tree).toList
println(inputs.map{showRaw(_)})
c.Expr[Any](Block(inputs, Literal(Constant(()))))
}
import scala.annotation.StaticAnnotation
class showMacro extends StaticAnnotation {
def macroTransform(annottees: Any*) = macro showMacroImpl
}
object Test {
@showMacro final val i = 1+1
@showMacro final val j = util.Random.nextInt()
@showMacro final val k = Int.MaxValue / 2
}
// List(ValDef(Modifiers(FINAL), newTermName("i"), TypeTree(), Apply(Select(Literal(Constant(1)), newTermName("$plus")), List(Literal(Constant(1))))))
// List(ValDef(Modifiers(FINAL), newTermName("j"), TypeTree(), Apply(Select(Select(Ident(newTermName("util")), newTermName("Random")), newTermName("nextInt")), List())))
// List(ValDef(Modifiers(FINAL), newTermName("k"), TypeTree(), Apply(Select(Select(Ident(newTermName("Int")), newTermName("MaxValue")), newTermName("$div")), List(Literal(Constant(2))))))
There is no difference between i
, j
and k
here.
You'll get no information even with scalac -Xprint:cleanup test.scala
:
final <stable> <accessor> def i(): Int = 2;
final <stable> <accessor> def j(): Int = Test.this.j;
final <stable> <accessor> def k(): Int = 1073741823;
You could get this information only from .icode
file (scalac -Xprint:all test.scala; cat Test\$.icode
):
def i(): Int(2) {
locals:
startBlock: 1
blocks: [1]
1:
2 CONSTANT(2)
2 RETURN(INT)
}
def k(): Int(1073741823) {
locals:
startBlock: 1
blocks: [1]
1:
4 CONSTANT(1073741823)
4 RETURN(INT)
}
Or from java bytecode (javap -c Test\$.class
):
public final int i();
Code:
0: iconst_2
1: ireturn
public final int k();
Code:
0: ldc #21 // int 1073741823
2: ireturn
You phrased your question as being about determining whether a value is being in-line expanded at points of reference, but it seems though you're actually looking for a way to guarantee it. Is that correct?
If you make it a def
that's annotated for in-line expansion (@inline
) you might get what you want.
@inline def TwentyTwo = 22