scala/scala3

Indirect access to `inline val` defined in `private object` causes `getter (...) was not inlined` error

Opened this issue · 8 comments

Compiler version

3.3.3
3.4.1

Minimized code

// object Konst:
private object Konst:
  inline val K = 1
  // inline def K = 1


object Use:
  // OK
  def a: Int = Konst.K

  // OK
  inline def b: Int = Konst.K

  // Error
  def c: Int = b

https://scastie.scala-lang.org/3bXUpYzlQ1eMnQOrLAOLuA

Output

[error] -- Error: /home/me/Bug.scala:17:15 
[error] 17 |  def c: Int = b
[error]    |               ^
[error]    |               getter K is declared as `inline`, but was not inlined
[error]    |
[error]    |               Try increasing `-Xmax-inlines` above 32
[error]    |----------------------------------------------------------------------------
[error]    |Inline stack trace
[error]    |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
[error]    |This location contains code that was inlined from Bug.scala:14
[error] 14 |  inline def b: Int = Konst.K
[error]    |                      ^^^^^^^
[error]     ----------------------------------------------------------------------------
[error] one error found

We can mask the error by either:

  • removing inlines
  • changing object Konst from private to public
  • changing val K to def K

Originally, Konst and Use were defined in separate files.

Expectation

Compile without error & apply inlining

This issue was picked for the Scala Issue Spree of tomorrow, June 11th. @hamzaremmal, @SethTisue and @rochala will be working on it. If you have any further insights into the issue or guidance on how to fix it, please leave it here.

-Vprint:all shows

[[syntax trees at end of MegaPhase{pruneErasedDefs, uninitialized, inlinePatterns, vcInlineMethods, seqLiterals, intercepted, getters, specializeFunctions, specializeTuples, collectNullableFields, elimOuterSelect, resolveSuper, functionXXLForwarders, paramForwarding, genericTuples, letOverApply, arrayConstructors}]]
...
  @SourceFile("S.scala") final module class Use() extends Object() {
...
    private inline def b: Int = scala.compiletime.erasedValue[Int]
    def c: Int = Use.inline$Konst.K:Int
    def inline$Konst: Konst = Konst
  }
}

I don't have much experience with inlining, but def inline$Konst: Konst = Konst seems odd to me. it seems like the idea is to evade the private-ness of Konst with a forwarder, but it isn't accomplishing that because the type is Konst anyway? (UPDATE: yes the forwarder is interfering, but because it's not stable; see below)

I guess let's enable tracing in inlining to get it talking about what it's doing? it might be useful to view the traces for the versions that work, as well as the trace for the version that goes wrong (UPDATE: this doesn't tell us anything additional that doesn't already show up in -Vprint:all, we can already see what is being inlined or not)

Not 100% confident this is accurate, but the following picture seems to be emerging (in discussion with Hamza and Jędrzej):

Both inlining itself and a separate constant-folding mechanism are in play here. The separate mechanism is the constant-folding that happens after inlining, in FirstTransform, which calls TreeInfo#constToLiteral. But def c: Int = Use.inline$Konst.K:Int cannot be constant-folded because inline$Konst isn't stable.

But we don't yet have an idea for a fix...

Recall that the semantics of inline val are very restrictive; inline vals must have constant literal types and be pure.

Fix idea: maybe instead of waiting for FirstTransform's constant-folding to go wrong later, after inlining, we can make the inliner actually inline K? (I think this dawned on all three of us at about the same moment...)

When you look at Inliner you see lots of code that seems intended to handle val and def the same (lots of ValOrDefDef), so we're puzzled why this plays out differently with def.

Jędrzej noticed that Typer has:

    if sym.isInlineMethod then rhsCtx.addMode(Mode.InlineableBody)

perhaps that's in play here? But we don't see where InlineableBody actually has any effect downstream?

(Is there any other such pre-analysis happening in Typer that's relevant?)

I think the issue is not caused by the bridges added due to the private modifier, but about selecting an inline val on the result of a non-inline method.

Minimisation:

class Foo:
  inline val F = 1

def foo(): Foo = ???
val _ = foo().F

Indeed, the issue looks very similar to what we have been seeing during the spree. I will have to investigate it further.