-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Different result of inlining if an inline parameter is a Unit #20082
-
I'm running into a behaviour I can't understand when using inlining and unit-typed inline parameters.
First, the code:
inline def pipe[T, U](inline t: T, inline f: T => U): U = f(t) def x(n: Int): Int = 10 def y(n: Unit): Int = 10
I've got a pipe
method (originally this was an extension method on t
, but here it's simplified) and two methods which I'll be using in tests. There's also a helper printTree
to debug the code that is being generated:
import scala.quoted.* inline def printTree[T](inline x: T): Unit = ${ printTreeImpl('x) } def printTreeImpl[T: Type](x: Expr[T])(using qctx: Quotes): Expr[Unit] = import qctx.reflect.* println(x.asTerm.show) '{ () }
Now, if I printTree(pipe((), y))
, I get:
({
val n: scala.Unit = ()
y(n)
}: scala.Int)
However, if I do printTree(pipe(10, x))
, I get:
(x(10): scala.Int)
So in the second case, the parameter is indeed inlined - as I would expect. But in the first one, it's first assigned to a val. Where does the difference come from?
Context
You might wonder - why would this matter? I arrived at the problem when the f
parameter to pipe
is itself and inlined function, taking inline parameters. Then, how the call is inlined makes a difference. Specifically, I've got:
inline def forever(inline f: Unit): Nothing = while true do f throw new RuntimeException("can't get here") extension [T](inline t: T) inline def pipe[U](inline f: T => U): U = f(t)
And then, forever { doSth() }
has different inlining result than doSth().pipe(forever)
(here, doSth
is invoked once, and forever loops on a unit).
Beta Was this translation helpful? Give feedback.
All reactions
This has to do with the implementation of the beta-reduction optimization.
inline def pipe[T, U](inline t: T, inline f: T => U): U = f(t) def test1 = pipe((), (u: Unit) => u) def test2 = pipe(10, (x: Int) => x)
These technically get inlined as
def test1 = ((u: Unit) => u).apply(())
def test2 = ((x: Int) => x).apply(10)
but when inlining we also do a best-effort beta-reduction optimization. Semantically, these reduce to
def test1: Unit = { val u: Unit = (); u }: Unit
def test2: Unit = { val x: Int = 10; x }: Int
But then we also attempt to elide redundant bindings for constants. This removes the val x
in test2
. The Unit
value is not exactly pure in bytecode terms as is ends up being a r...
Replies: 1 comment 10 replies
-
This has to do with the implementation of the beta-reduction optimization.
inline def pipe[T, U](inline t: T, inline f: T => U): U = f(t) def test1 = pipe((), (u: Unit) => u) def test2 = pipe(10, (x: Int) => x)
These technically get inlined as
def test1 = ((u: Unit) => u).apply(())
def test2 = ((x: Int) => x).apply(10)
but when inlining we also do a best-effort beta-reduction optimization. Semantically, these reduce to
def test1: Unit = { val u: Unit = (); u }: Unit
def test2: Unit = { val x: Int = 10; x }: Int
But then we also attempt to elide redundant bindings for constants. This removes the val x
in test2
. The Unit
value is not exactly pure in bytecode terms as is ends up being a reference to scala.runtime.BoxedUnit.UNIT
. However, it would make sense to optimize it as well.
In general, there is no guarantee that the bindings of the beta-reduction will be elided or not.
Beta Was this translation helpful? Give feedback.
All reactions
-
Thanks! Really good explanations :) Yeah I already caught myself doing the mistake of inlining too much in my head ;) Make sense of course that any reductions that the compiler applies can't change the semantics of the program. And it also makes sense that inlines are applied first, then reductions happen, as is the case in the example above (so neither branch1 or branch2 from my previous post are correct).
Then I suppose with the current inline
options, the pipe
function as implemented above isn't really equivalent (syntactically) to writing the application directly, that is:
f(x)
isn't equivalent to
x.pipe(f)
a counter-example being work().pipe(forever)
which behaves differently than forever(work())
. That is, a simple copy-paste mental model of how inlining works fails here. But unless I'm missing something, the lack of equivalence seems to be limited to functions taking inline
or by-name parameters?
Other than InlineFunction1WithInlineParam
(which would require changing each inlining function), I guess a macro version of pipe
which would do the beta-rewrite would also work? Though the question then is, wouldn't it be too surprising for users.
Beta Was this translation helpful? Give feedback.
All reactions
-
I believe that semantically you would need something like this to represent the reduction that you want.
inline def forever(inline f: => Unit): Nothing = while true do f throw new RuntimeException("can't get here") extension [T](inline t: T) inline def pipe[U](inline f: (=>T) => U): U = f(t)
This tells the pipe f
that it does not need to evaluate the argument before evaluating the application. Therefore the beta-reduction should not create the val
binding (it might create a def
binding). However there seems to be a bug with this reduction, I will investigate further (see #20095).
Beta Was this translation helpful? Give feedback.
All reactions
-
❤️ 1
-
Thanks! For 3.3 I guess I'll try with the macro, as I suspect these fixes will go into 3.4+.
And for inlines such as forever
, we'll still get the additional def
, right? So x.pipe(forever)
will be different than forever(x)
(though it will work, which is an important aspect :) ).
Beta Was this translation helpful? Give feedback.
All reactions
-
#20096 can be backported to a 3.3 patch release.
Beta Was this translation helpful? Give feedback.
All reactions
-
Thank you @nicolasstucki for tackling this. 😀
My helper works well now:
//> using scala 3.5.0 import scala.annotation.targetName extension [A](x: => A) @targetName("pipe") inline def |>[B](inline f: A => B): B = f(x) @targetName("pipeByName") inline def |>[B](inline f: (=> A) => B): B = f(x)
Beta Was this translation helpful? Give feedback.