scala 3 inlining fails a very simple example

Viewed 261

I'm exploring the possibilities of scala3's inlining. I've made up a simple example where I want a validation method constructing a sealed class to be applied at compile time:

import scala.compiletime.{ error, codeOf }

sealed abstract class OneOrTwo(val code: String)

object OneOrTwo {

    case object One extends OneOrTwo("one")
    case object Two extends OneOrTwo("two")

    def from(s: String): Option[OneOrTwo] = 
        s match {
            case One.code => Some(One)
            case Two.code => Some(Two)
            case _ => None
        }

    inline def inlinedFrom1(s: String): OneOrTwo = {
        inline s match {
            case "one" => One
            case "two" => Two
            case _ => error("can't make a OneOrTwo out of " + codeOf(s))
        } 
    }

    val test1 = OneOrTwo.inlinedFrom1("one") // compiles 
    val test12 = OneOrTwo.inlinedFrom1("three") // doesn't compile as expected -> can't make a OneOrTwo out of "three"
}

So far, so good. But what I really want is to be able to re-use the from function within the inlined one. Here's me trying:

    // this goes in the OneOrTwo companion object as well

    inline def inlinedFrom2(s: String): OneOrTwo = {
        from(s).getOrElse(error("can't make a OneOrTwo out of " + codeOf(s)))
    }

    inline def inlinedFrom3(s: String): OneOrTwo = {
        s match {
            case One.code => One
            case Two.code => Two
            case _ => error("can't make a OneOrTwo out of " + codeOf(s))
        }
    }

    
    val test2 = OneOrTwo.inlinedFrom2("one") // doesn't compile -> can't make a OneOrTwo out of "one"
    val test3 = OneOrTwo.inlinedFrom3("one") // doesn't compile -> can't make a OneOrTwo out of "one"

inlinedFrom3 is a generalisation to show that if I match on the objects' code directly, the compiler doesn't see that they are the same as the input string and takes the wrong branch.

What I'd like to understand is why inlinedFrom3 doesn't work and whether there is a way to make it work.

Note: I'm using scala 3.0.0-RC2

1 Answers

Good question,

First of all, consider that inlining is a way to perform meta-programming at compile time in Scala (as you have done in your example).

To get bet on your problem, here the compiler doesn't understand that One.code is of type "one". Indeed, if you try to print One.code, the compiler output is:

OneOrTwo.One.code

and not

"one"

But wait, why? Because the compiler can't infer anything about the One.code at compile time. To give the missing information to the compiler you can:

  • specify the singleton string type
sealed trait OneOrTwo {
  val code: String
}

object OneOrTwo {
    case object One extends OneOrTwo {
      override val code : "one" = "one" 
    }
    case object Two extends OneOrTwo {
      override val code : "two" = "two"
    }
}
  • inline val definition
sealed trait OneOrTwo {
  val code: String
}

object OneOrTwo {
    case object One extends OneOrTwo {
      override inline val code = "one" 
    }
    case object Two extends OneOrTwo {
      override inline val code = "two"
    }
}

Even with this modification, your code continues to fail in the compilation. It is caused by the match clause not inlined. Indeed, as mentioned here, the error method provides a way to emit custom error messages. The error will be emitted if a call to error is inlined and not eliminated as a dead branch. So if you put error(...) in a code not inlined, an error will be always thrown. For example, this code:

if(false) { error("thrown..") } { }

produces a compilation error.

Finally, to solve your problem, you need to inline your match clause:

inline def inlinedFrom3(s: String): OneOrTwo = {
   inline s match {
       case One.code => One
       case Two.code => Two
       case _ => error("can't make a OneOrTwo out of " + codeOf(s))
   }
}

The compilation (for inlinedFrom3) succeeded. inlineFrom2 continues to fail due to the problem aforementioned.

Related