11

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.

Mario Galic
  • 47,285
  • 6
  • 56
  • 98
Ed Staub
  • 15,480
  • 3
  • 61
  • 91

3 Answers3

11

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
Eugene Burmako
  • 13,028
  • 1
  • 46
  • 59
3

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       
senia
  • 37,745
  • 4
  • 88
  • 129
  • An A for effort. I thought maybe one somewhat ugly way of doing it might be a _following_ annotation that takes the constant as an argument. As I understand it, at least in Java, integer arguments to annotations must be compile-time constants. The ugly part is that, AFAIK, the annotation would have to have a dummy "subject" of some kind. – Ed Staub Jan 20 '14 at 21:11
  • @EdStaub: note that `Test.i` is not a constant, it's a method that returns constant. – senia Jan 20 '14 at 21:13
  • Ouch, yeah, thanks, I missed it (that Test.i is a method). I guess I need to look at potential compile-time uses of the val, to see whether the method gets invoked or the compiler short-circuits it and just plugs in the constant value. If it isn't skipped, my question is pointless. – Ed Staub Jan 20 '14 at 21:20
  • 1
    `1 + 1` gets const-folded when it's typechecked. Macro annotations don't typecheck their annottees, so `1 + 1` remains as is. If you use a def macro, e.g. `final val i = ensureConstant(1 + 1)`, then you would see `Literal(Constant(2))`. – Eugene Burmako Jan 20 '14 at 21:21
  • 1
    You know what, it looks like the typechecker can constfold the code provided by the OP, so def macros look feasible for the task at hand! – Eugene Burmako Jan 20 '14 at 21:23
  • @EugeneBurmako Thanks much - I haven't touched macros yet, guess this is the time. – Ed Staub Jan 20 '14 at 21:26
  • http://stackoverflow.com/questions/21242428/is-there-a-way-to-test-at-compile-time-that-a-constant-is-a-compile-time-constan/21244494#21244494 – Eugene Burmako Jan 20 '14 at 21:27
2

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
Randall Schulz
  • 26,420
  • 4
  • 61
  • 81
  • 1
    1. You could use `val` here: `@inline val TwentyTwo = 22`. 2. No, it doesn't makes `TwentyTwo` a constant in terms of `java` bytecode. Try `@inline val j = util.Random.nextInt()` - it's definitely not a compile time constant. – senia Jan 20 '14 at 20:29
  • Yes, thanks, I want a way to guarantee it. I'm doing some arithmetic-intensive work with large collections that needs to be extremely high-performance. So I want to be sure that the value of the constant is accessible to the compiler at compile-time. – Ed Staub Jan 20 '14 at 21:04
  • Obviously if there are parameters or side-effects in the method body it's not going to be anything you'd call a constant, but I assume the questioner would not do that, it invalidates the whole premise of the question. – Randall Schulz Jan 20 '14 at 23:24