3

Given the following macro (thanks @TravisBrown for this help ):

JetDim.scala

case class JetDim(dimension: Int) {
  require(dimension > 0)
}

object JetDim {
  def validate(dimension: Int): Int = macro JetDimMacro.apply
  def build(dimension: Int): JetDim = JetDim(validate(dimension))
}

JetDimMacro.scala

import reflect.macros.Context

object JetDimMacro {

    sealed trait PosIntCheckResult
    case class LteqZero(x: Int) extends PosIntCheckResult
    case object NotConstant extends PosIntCheckResult

    def apply(c: Context)(dimension: c.Expr[Int]): c.Expr[Int] = {

        import c.universe._

        getInt(c)(dimension) match {
            case Right(_)          => reify { dimension.splice }
            case Left(LteqZero(x)) => c.abort(c.enclosingPosition, s"$x must be > 0.")
            case Left(NotConstant) => reify { dimension.splice }
        }
    }

    def getInt(c: Context)(dimension: c.Expr[Int]): Either[PosIntCheckResult, Int] = {

        import c.universe._

        dimension.tree match {
            case Literal(Constant(x: Int)) => if (x > 0) Right(x) else Left(LteqZero(x))
            case _                         => Left(NotConstant)
        }
    }
}

It works from the REPL:

scala> import spire.math.JetDim
import spire.math.JetDim

scala> JetDim.validate(-55)
<console>:9: error: -55 must be > 0.
              JetDim.validate(-55)
                             ^

scala> JetDim.validate(100)
res1: Int = 100

But, I'd like to build this compile-time check (via the JetDimMacro) into the case class's apply method.

Attempt 1

case class JetDim(dimension: Int) {
  require(dimension > 0)
}

object JetDim {
  private def validate(dimension: Int): Int = macro JetDimMacro.apply
  def build(dimension: Int): JetDim = JetDim(validate(dimension))
}

But that failed:

scala> import spire.math.JetDim
import spire.math.JetDim

scala> JetDim.build(-55)
java.lang.IllegalArgumentException: requirement failed
  at scala.Predef$.require(Predef.scala:207)
  at spire.math.JetDim.<init>(Jet.scala:21)
  at spire.math.JetDim$.build(Jet.scala:26)
  ... 43 elided

Attempt 2

class JetDim(dim: Int) {
  require(dim > 0)

  def dimension: Int = dim
}

object JetDim {
  private def validate(dimension: Int): Int = macro JetDimMacro.apply
  def apply(dimension: Int): JetDim = {
    validate(dimension)
    new JetDim(dimension)
  }
}

Yet that failed too:

scala> import spire.math.JetDim
import spire.math.JetDim

scala> JetDim(555)
res0: spire.math.JetDim = spire.math.JetDim@4b56f205

scala> JetDim(-555)
java.lang.IllegalArgumentException: requirement failed
  at scala.Predef$.require(Predef.scala:207)
  at spire.math.JetDim.<init>(Jet.scala:21)
  at spire.math.JetDim$.apply(Jet.scala:30)
  ... 43 elided

I thought to modify JetDimMacro#apply to return a JetDim rather than an Int. However, JetDim lives in the core project, which, from what I see, depends on the macros project (where JetDimMacro lives).

How can I use this validate method from JetDim's companion object to check for positive int's at compile-time?

1 Answer 1

1

The problem is that by the time we call validate in apply we are no longer dealing with a constant (singleton type). So, validate gets a non-constant Int.

As an alternative, you could try using an implicit witness for positive ints, which JetDim then takes as a constructor. For instance, something like:

package com.example

case class JetDim(n: PositiveInt)

case class PositiveInt(value: Int) {
  require(value > 0)
}

Then, we add an implicit (macro) conversion from Int => PositiveInt that does your check.

import scala.language.experimental.macros

import scala.reflect.macros.blackbox.Context

object PositiveInt {
  implicit def wrapConstantInt(n: Int): PositiveInt = macro verifyPositiveInt

  def verifyPositiveInt(c: Context)(n: c.Expr[Int]): c.Expr[PositiveInt] = {
    import c.universe._

    val tree = n.tree match {
      case Literal(Constant(x: Int)) if x > 0 =>
        q"_root_.com.example.PositiveInt($n)"
      case Literal(Constant(x: Int)) =>
        c.abort(c.enclosingPosition, s"$x <= 0")
      case x =>
        c.abort(c.enclosingPosition, s"cannot verify $x > 0")
    }
    c.Expr(tree)
  }
}

You can then use JetDim(12), which will pass, or JetDim(-12), which will fail (the macro expands the Int to a PositiveInt).

Sign up to request clarification or add additional context in comments.

2 Comments

Thank you. With this approach, the PositiveInt companion object, which contains the macro impl, would live in the same class as JetDim, no? The reason I'm asking is if we want to use PositiveInt elsewhere.
@KevinMeredith Yeah, it would generally be useful elsewhere, so it'd make sense to have it live by itself so other methods/classes can use it.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.