Can we have inline if/match in normal methods?

Hi,

this is a papercut when using inline if/match for conditional compilation.
The following does not work, because inline if/match is not allowed within non-inline methods:

Scastie:

  inline val enableFeature = false

  def main(args: Array[String]): Unit = {
    inline if enableFeature
    then println("feature enabled")
    else println("feature disabled")
  }

The workaround is very simple, just use an inner inline def, but it is annoying to have to define a name:

Scastie:

  inline val enableFeature = false

  def main(args: Array[String]): Unit = {
    inline def workaround() =
      inline if enableFeature
      then println("feature enabled")
      else println("feature disabled")
    workaround()
  }

I don’t see a reason why the compiler should not be able to produce the same result for both variants.

I find this pattern useful as a form of conditional compilation, where the inline val is usually defined by a source generator, or conditionally included in the sources depending on the compilation target (e.g.: js, native, jvm).

3 Likes

This will accomplish the same thing, actually :slight_smile:

final val enableFeature = false

  def main(args: Array[String]): Unit = {
    if enableFeature
    then println("feature enabled")
    else println("feature disabled")
  }

Thanks for the suggestion! That is indeed an alternative workaround for the cross compilation case.

[EDIT: constant value definitions are in the spec here: Basic Definitions | Scala 3.4 and have to have the form final val x = e (no type annotation) I was not aware that this is a special thing in the language … not sure if that has impact on the below, still not sure how they are treated specially. It seems that constant expressions are “platform dependent” which seems to align with the reasoning below. I did not find anything in the spec about constant expressions being treated differently for control flow.]

If someone else is interested I did test with Scala 3.6.2 and ScalaNative 0.5.6 and the following seem to cause enough optimizations in the branch such that any symbols accessible through the non-taken branch do not make the linker complain:

inline val enableFeature = false
final val enableFeature = false
val enableFeature: false = false

Adding a type annotation breaks things:

final val enableFeature: Boolean = false

For the linker checked case that is probably fine, as the linker will complain. inline val will require the type to be a singleton, so just using that instead of final val also prevents this accident.

However, relying on plain if is hoping that the compiler applies some optimization, not relying on language semantics.

Particularly, this is notable from any other inline/metaprogramming facilities.

While this is probably a bit specific, I actually do have a program where this matters. It disables some features when compiling with scala-native. This changes the available command line options, which are generated by a macro that essentially reads the cases in the main method.


Slightly related, playing around a bit with the different final val variants, when things work and when not was super hard to predict.
This works:

  final val enableFeature = true

  def main(args: Array[String]): Unit = {
    inline def workaround() =
      inline val y = if enableFeature then 1 else 2
      y
    println(workaround())
  }

This does not:

  final val enableFeature = true

  def main(args: Array[String]): Unit = {
    inline val y = if enableFeature then 1 else 2
    println(y)
  }

Maybe because of the staged compilation for inline defs the first case actually has the optimization applied before any of the other checks on the types are done?

I think it would be nice to address papercuts in the inline based metaprogramming as much as possible, because it’s such an elegant system at its core : - )

Sample recent discussion on constant value definitions.

The feature just needs a champion to step in and reply whenever someone asks about it.

I happened to just notice that annotation.elidable has an example for Scala 3 users at the Scaladoc.

I have always understood simplification of the conditional to be a language feature and not an optimization.

It is has a pinky-swear promise in the reference for inline.

I also understand inline if/match simplification is a language feature.

I could not find anything about final val constant expressions being optimized as a language feature – though final vals do cooperate with inline if. If only the latter did not need that workaround :stuck_out_tongue_winking_eye:.

I meant to say also in Scala 2. Such branch elimination is always applied.

if (true) 42 else 27

is 42.

It works for inlined final val but not for

val b: true = true

or a param of literal type.

That might be just a bug, but maybe that is not considered foldable.