-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Inlining or quoting generic types causes boxing #11998
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
That's not the case. Inlining does not revise a choice of overloaded method made in the inlined function. I had a design that did this and it looked to have great potential for optimizations. But the downside would have been that we cannot guarantee type safety or semantics preservation in inlined code. So we backed out of that. If you need more finegrained control what code should be generated, your best bet is staging. I.e. work with splices. |
Aww shame, that other design would've been awesome. I hope it can come back one day. |
I just tried that and it's the same problem: private object OpsMacros:
def eqImpl[A: Type](a: Expr[A], b: Expr[A])(using Quotes): Expr[Boolean] =
'{ $a == $b }
extension [A](inline a: A)
inline def ==*[B >: A](inline b: B)(using ev: => UnivEq[B]): Boolean =
${ OpsMacros.eqImpl[B]('a, 'b) }
def test(a: Boolean) = a ==* a
@odersky @nicolasstucki This is really unexpected but even with quotes and splices it boxes. Surely this is a bug, isn't it? |
This seems correct to me. As is, there is a single quote that works with a generic But all of that seems to miss the bigger picture to me. Even a generic |
Can Scala 2 compile the example without boxing? |
I've tried with the following, and it does not seem like it manages to go that far, unfortunately. scalaVersion := "2.13.5"
scalacOptions ++= Seq(
"-opt:l:inline",
"-opt-inline-from:foo.**",
) package foo
object App {
def main(args: Array[String]): Unit = {
println(System.getProperty("java.vm.version"))
val test = new Test
println(test.x1)
println(test.x2)
println(test.x3)
println(test.z1)
}
}
object Ops {
implicit class EqOps[A](private val a: A) extends AnyVal {
@inline
def ==*(b: A): Boolean = a == b
}
}
import Ops._
class Test {
var i = 1
var res = false
def x1 = res = 1 ==* 1
def x2 = res = 1 ==* 2
def x3 = res = 1 ==* i
def z1 = res = 1 == i
}
However, IMO this should be fixable. I'm pretty sure it has something to track what values are boxes of something, and it could intrinsify calls to In fact, even the Scala.js optimizer doesn't do that intrinsification today; we should add that when we get the chance. But the fact remains that this is something that is doable in a back-end optimizer, in a way that is not harder than in the front-end. |
Here's an additional test / datapoint: import scala.quoted.*
object Target {
def blah(a: Int): Int = 1
def blah(a: Any): Int = 2
}
object Macros:
inline def test[A](inline a: A): Int =
${ macroImpl[A]('a) }
def macroImpl[A: Type](a: Expr[A])(using Quotes): Expr[Int] =
'{ Target.blah($a) }
@main def main = {
println(s"Direct: ${Target.blah(1)} | ${Target.blah("")}")
println(s"Via Quotes: ${Macros.test(1)} | ${Macros.test("")}")
} It prints:
It seems to me that a potential fix is:
|
@japgolly You can think about why it's not possible the following way. Consider: def foo(x: Int): Int = x
def foo(x: Any): String = x.toString The quote We can't just haphazardly retype things with more specific types, as that could break programs in arbitrary ways. Also see this related issue: #11924 If I were you, I would favor the use of macro methods which can select the efficient implementation by case analysis on the type of the argument. In the case of methods which you don't control, like |
@LPTK Yeah good point. That just means we need to narrow the surface area a little though; it doesn't mean the entire concept needs to go to the bin. I imagine the most basic way to avoid problems would be to:
|
Sorry @LPTK I completely missed your last paragraph somehow. Reading it I see we have an approach in mind that is mostly the same, just that my idea is to apply that to the quoting machinery so everyone gets it for free (which is highly likely to be what users expect/assume would happen anyway). |
We can't do that. While it applies to Re-elaborating is a big no-no. It breaks semantics, no matter how one wants to look at it. Quotes are elaborated once, like "normal" code; we can't just re-elaborate them later. |
In the example above, the simplest solution seem to be to do something like def macroImpl[A: Type](a: Expr[A])(using Quotes): Expr[Int] =
a match
case '{ $a: Int } => '{ Target.blah($a) } // chose `blah` overload for Int
case _ => '{ Target.blah($a) } This way each quote |
IMO this is behaving the opposite of "normal" code. For lack of a better term, "true inlining" doesn't care about types or semantics; it simply serves as compile-time substitution. Like in C, something like |
What you're describing is macro expansion, not inlining, even in C. True inlining definitely cares about types and semantics. It's the whole point of inlining versus untyped macro expansion. That you can confidently write your library in way that is guaranteed to work, irrespective of in which conditions your users call it. |
What you refer to as "true inlining" is syntactic inlining, this kind of inlining is found in preprocessor such as Scala 3 uses semantic inlining. |
Their specilized versions are known to have the same semantics as the generic `==` or `!=`. Improvement related to scala#11998
Right, that makes sense! (And thank you both for taking the time to clarify, TIL.) Ok so let me back up a little bit and go step by step. |
For reference, to avoid boxing in the Scala 2 world I simply used |
No, one could say it's in the semantic macro expansion category :^) It's designed to bring together the advantages of semantic inlining (reliability and predictability) with the extra expressiveness and control of macro expansion. It is patently not designed to just cobble random pieces of syntax together to see if they stick after the macro has expanded, which is what C-style "unhygienic" macros do, and result in terrible user experience. One of the main complaints in Scala 2 macro was the bad user experience of broken macros, which raise seemingly-meaningless type errors when they fail, and where you have no way of telling what's wrong short of printing the expanded code and reading through unintelligible computer-generated nightmare.
Interested to know of any examples of this. Note that we're not talking about the underlying lower-level trees API here, which is more powerful, though with fewer guarantees, and which will re-type things in the way you want. I still think it's a really bad idea to try and retype everything all the time. It's kind of like people who discover implicit conversions for the first time, think they are so useful, and want to use them everywhere, only to realize that this eventually leads to unmaintainable hell. As for what you want to do, as was already pointed out, it would be a lot better as a compiler pass (perhaps in a compiler plugin). |
Their specilized versions are known to have the same semantics as the generic `==` or `!=`. Improvement related to scala#11998
There are some cases that might be miscategorized as not type checking after inlining such as the use of |
Compiler version
3.0.1-RC1-bin-20210402-775d881-NIGHTLY
Minimized code
Output
Expectation
We can see in the disassembly of
x3
that it boxes the Ints. I was expecting thatinline
would choose the appropriate implementation of==
based on the types of its arguments. I started down the path of matching onerasedValue[Int]
to provide more type info to the inliner but that didn't make a difference.Workaround
The following generates code that doesn't box but considering that I'd have to do it for every primitive, for every inline method I write, it would very quickly become an unreasonable amount of work and boilerplate.
The text was updated successfully, but these errors were encountered: