Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Different result of inlining if an inline parameter is a Unit #20082

Discussion options

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).

You must be logged in to vote

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

Comment options

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.

You must be logged in to vote
10 replies
Comment options

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.

Comment options

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).

Comment options

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 :) ).

Comment options

#20096 can be backported to a 3.3 patch release.

Comment options

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)
Answer selected by adamw
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

AltStyle によって変換されたページ (->オリジナル) /