scala3 icon indicating copy to clipboard operation
scala3 copied to clipboard

inline match breaks simple example, documentation?

Open Adam-Vandervorst opened this issue 2 years ago • 10 comments

Compiler version

3.0.2

Minimized code

(transparent) inline def f(x: Int) = inline x match
  case 1 => 1
  case _ => 0

@main def m =
  val v = 1
  println(f(v))

Output

0

Expectation

Give the same output as (namely 1)

inline def f(x: Int) = x match
  case 1 => 1
  case _ => 0

and if not, be documented in the "Inline Matches" section of https://docs.scala-lang.org/scala3/reference/metaprogramming/inline.html.

(Note that this does work when v has singleton type 1)

Adam-Vandervorst avatar Oct 19 '21 12:10 Adam-Vandervorst

I suspect the following issue is related to this:

trait X[T]:
  val value = "X"

given X[Int]()

transparent inline def f[T](x: T) = summonFrom {
  case xt: X[T] => xt.value
  case _ => "None"
}

transparent inline def g[Tup <: NonEmptyTuple](t: Tup) = f(t.head)

transparent inline def h[Tup <: NonEmptyTuple](t: Tup) = t match
  case (head *: _) => f(head)

@main def m =
  val t = (1, "s")
  println(g(t))
  println(h(t))

returns different values for g and h. However, an inline match doesn't work because

cannot reduce inline match with
 scrutinee:  t : (t : (Int, String))
 patterns :  case *:.unapply[Any, Tuple](head @ _, _):Any *: Tuple
transparent inline def h[Tup <: NonEmptyTuple](t: Tup) = inline t match

Adam-Vandervorst avatar Oct 19 '21 12:10 Adam-Vandervorst

I found this single line in the "Semantics-Preserving Inlining for Metaprogramming" paper on the topic:

Unlike the inline if, this reduction is not necessarily equivalent to its runtime counterpart when we have more type information.

which is about match not being semantics-preserving. I guess this reduces this issue to updating the docs?

Adam-Vandervorst avatar Oct 19 '21 13:10 Adam-Vandervorst

We can mention that inline match works on the static type of an expression, not its runtime type, hence matching the wildcard case when v == 1 but its type is Int

bishabosha avatar Oct 19 '21 13:10 bishabosha

That makes it so a chain of calling -> inline match -> summonFrom like in https://github.com/lampepfl/dotty/issues/13774#issuecomment-946696449 can never work? The reason being that inline match works on static types, hence can be evaluated before summonFrom, hence leaves the types not specific enough for that to work?

Adam-Vandervorst avatar Oct 19 '21 13:10 Adam-Vandervorst

The documentation says If there is enough static information to unambiguously take a branch. In this case we only know that the type of x is Int. This implies that it is ambiguous as if the value is 1 then either branch would work and therefore we should not be able to reduce the inline match.

nicolasstucki avatar Oct 26 '21 09:10 nicolasstucki

This came up last year at #10919, where @odersky said the behavior is not just intended, but "specified".

But I really don't see any wording at https://docs.scala-lang.org/scala3/reference/metaprogramming/inline.html that specifies it.

In fact, as Nicolas observes, "If there is enough static information to unambiguously take a branch" (wording from that page) can be read as requiring that compilation fail, since the cases overlap.

SethTisue avatar Sep 21 '22 14:09 SethTisue

I agree with you @SethTisue. Assuming the doc is correct in saying:

"A match expression in the body of an inline method definition may be prefixed by the inline modifier. If there is enough static information to unambiguously take a branch, the expression is reduced to that branch and the type of the result is taken. If not, a compile-time error is raised that reports that the match cannot be reduced."

My conclusion (please challenge this) is that inline match is supposed to preserve semantics (like inline if does), but it doesn't.

Is it safe to assume we need a fix to reflect this semantic preserving behaviour?

In case this assumption is wrong, @odersky can you please clarify what you mean in very old #10919, what you say the behaviour is not just intended, but specified?

antognini avatar Sep 22 '22 12:09 antognini

Speaking as a non-expert, which makes me an audience for the simple example, I think the doc is quite clear.

However, I would remove the weaselly adverb, "unambiguously". I would also prefer "select" or "pick" to "take". I'd also prefer not to split an infinitive. (I also double-checked, the character's name is Ron Weasley.)

If there is enough static information to select a branch, the expression is reduced...

It already says "static type":

The example below ... picks a case based on its static type

But I sympathize with the OP, where there was not enough inlining to have the salutary effect. 🖖

Maybe a lint could warn about non-unambiguously specified inline matches. In the example, if a branch matches on a narrow type (1) but the (inlined) scrutinee is a wider type (Int), then annoy me by pointing that out. Maybe the lint is only triggered by singleton types (which might suggest "I was expecting runtime-like semantics").

Also, on the semantics of "semantics", to speak of "preserving" them, one must distinguish "compiletime" and "runtime", a difference encapsulated by the word "static". Unfortunately, that word is overloaded: connotations accrue to it as by "static cling".

If there is enough type information at compile time to select a branch, the expression is reduced...

That may be usefully redundant.

som-snytt avatar Sep 22 '22 19:09 som-snytt

Surprisingly warning-free

transparent inline def f(x: Any): Any =
  inline x match
    case x: Any => (x, x)
    case x: Any => x

som-snytt avatar Sep 22 '22 19:09 som-snytt

I "committed" my formulation in the proposed PR, but I'm open to suggestions and amendments or emendations that are not mendacious.

som-snytt avatar Sep 22 '22 19:09 som-snytt