Skip to content

Inliner fails to properly reduce pure case classes in a few situations #8306

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

Closed
fhackett opened this issue Feb 13, 2020 · 5 comments · Fixed by #8348
Closed

Inliner fails to properly reduce pure case classes in a few situations #8306

fhackett opened this issue Feb 13, 2020 · 5 comments · Fixed by #8348

Comments

@fhackett
Copy link
Contributor

Setup:

case class A(i: Int)
case class B(a: A)

Note: results are paraphrased, removing some of the more verbose types and qualifications. Assume all the code snippets below are inside some arbitrary inline method that has A and B in scope.

The expected result for all cases is:

3

Inline match around type ascriptions

inline (A(3) : A) match {
  case A(i) => i
}

Result:

val $scrutinee1 = A(3) : A
val $elem1 = $scrutinee1.i
val i = $elem1
i

Inline match with nested patterns

inline B(A(3)) match {
  case B(A(i)) => i
}

Result:

val $scrutinee1 = B(A(3))
val $elem1 = A(3) // so clearly it's trying, it just doesn't keep going...
val $elem2 = $elem1.i
val i = $elem2
i

Notice how it keeps $scrutinee1 around despite no references to it, which might be a bug on its own.

Projecting without an inline match

A(3).i

Result:

A(3).i

It's unclear if this last one is by design, but since this kind of compile-time case class manipulation/destructuring feels like constant folding (and basically is), I've included it.

@fhackett
Copy link
Contributor Author

I found an extra case under the same theme:

When the macro is accessed via the cake pattern

trait Cake {
  case class A(i: Int)
  inline def dummy: Int =
    inline A(3) match {
      case A(i) => i
    }
}
object Test extends Cake {
  println(code"${ dummy }")
}

Result:

val Cake_this : Test.type = this
{
  val $scrutinee1 = Cake_this.A(3)
  val i : 3 = 3 // notice the extraction does actually work...
                // problem is a lot of stuff is left after
  i
}

Note: the same macro definitions reduce properly when accessed directly.

@fhackett
Copy link
Contributor Author

I've discovered that the cases where $scrutinee stayed in place are not what I thought.

Actually, there is not a single case where the construction of A is elided. It's actually the .show pretty printer deciding it can hide all the redundant code some times and not others. If you inspect even the simple case (inline A(3) match { case A(i) => i }) at the bytecode level all the calls are actually still there.

The reason for this is the purity check - the case class constructions are considered impure and thus not considered for removal by dropUnusedDefs.

Given my interest in this issue is precisely because I want all that redundant code to go away, I will try to change that and see what happens.

That said, I'm almost done fixing the other logic bugs causing most of the cases in this issue to not even appear to work. For ease of reviewing I will probably make 2 PRs, one with the little inliner logic fixes and one with whatever comes of my attempts at tweaking the purity checker.

I'm learning all this as I go, so comments / criticism are welcome (especially if I missed some tricky internal compiler semantics!).

@fhackett
Copy link
Contributor Author

One more comment before I turn in for the night: what I found from some experiments with the purity checking code.

It seems safe to add one extra case to exprPurity. Specifically, extending the Apply case with an adaptation of the same case class checking code from the inliner.

I found an existing case that covers direct constructor calls to classes that satisfy isNoInitsClass (which was helping the inliner skip direct constructor calls only). I could not directly add to that case as that also checks exprPurity(fn), which turns out to be trivially false for X.apply. Because of all the information we have about this specific apply method though: synthetic, case class itself has no constructor; we can expect the case class to follow convention and, as far as I can tell, the purity check on fn can be skipped entirely.

Perhaps this does not need 2 PRs in the end - it's much less of a breaking change than I feared.

TODOs I'm leaving for tomorrow:

  • the cake pattern case
  • projections without an inline match (maybe that will also not be too hard / breaking and I can just put it in the PR for comment)

@fhackett
Copy link
Contributor Author

I take back my previous update about just declaring case class applications to be pure... they're not, because accessing the apply method can cause object initialisation. 😢

So in actuality this change shouldn't be touching the purity checker - it should be special-casing these purity checks only for the inliner. As such I'll be refactoring my change into the inliner, calling it isEffectivelyPureExpr or something similar.

On the bright side, once the inliner thinks case class application is pure (and no-one else does!) all the other problems in this issue seem to have working fixes. The cake pattern case is fixed by the case class application special case, and causing case class projections to inline without a match was not a particularly problematic change.

fhackett added a commit to fhackett/dotty that referenced this issue Feb 20, 2020
…ass applications in various situations

This is a collection of related changes, all to ensure that the inliner is capable of properly eliding case class
constructions in various situations.

Specific intended changes:
- allow NewInstance.unapply to look past type ascriptions
- change all purity checks to consider, for the inliner only, case class applications (when the apply method is synthetic
  and the case class has no constructor body) pure, or "elideable", despite the fact that the operation is almost always
  idempotent in the general case.
- make reduceInlineMatch's reduceSubPatterns procedure set the defTree for the accessor projections it generates, so nested
  Unapply patterns can look in defTree to find and possible inline the appropriate subtree of the scrutinee expression
- make the typedSelect override able to reduce projections without a corresponding inline match, so that if inlining
  produces SomeCaseClass(x=3).x the case class construction has a change to be elided (only in an inline context)

This commit also includes a series of test cases which use compiletime.show to pretty-print the resulting inlined AST of a
series of inline expansions. To avoid cases where a malformed AST pretty-prints but causes bad codegen, the computed
run-time value is also printed.

One last thing I tested manually was that the bytecode matched the pretty-printing. Mid-work on this commit I found that
the pretty printer was hiding several cases where redundant code was left in but not showing up when pretty-printed. This
is partially tested by the case where full inlining is not possible, which would also include a redundant binding for the
scrutinee if that issue were still present.
@fhackett
Copy link
Contributor Author

PR is up: #8348. Not sure from whom to request feedback.

Since you labeled this issue, any comments @odersky?

fhackett added a commit to fhackett/dotty that referenced this issue Apr 6, 2020
…ass applications in various situations

This is a collection of related changes, all to ensure that the inliner is capable of properly eliding case class
constructions in various situations.

Specific intended changes:
- allow NewInstance.unapply to look past type ascriptions
- change all purity checks to consider, for the inliner only, case class applications (when the apply method is synthetic
  and the case class has no constructor body) pure, or "elideable", despite the fact that the operation is almost always
  idempotent in the general case.
- make reduceInlineMatch's reduceSubPatterns procedure set the defTree for the accessor projections it generates, so nested
  Unapply patterns can look in defTree to find and possible inline the appropriate subtree of the scrutinee expression
- make the typedSelect override able to reduce projections without a corresponding inline match, so that if inlining
  produces SomeCaseClass(x=3).x the case class construction has a change to be elided (only in an inline context)

This commit also includes a series of test cases which use compiletime.show to pretty-print the resulting inlined AST of a
series of inline expansions. To avoid cases where a malformed AST pretty-prints but causes bad codegen, the computed
run-time value is also printed.

One last thing I tested manually was that the bytecode matched the pretty-printing. Mid-work on this commit I found that
the pretty printer was hiding several cases where redundant code was left in but not showing up when pretty-printed. This
is partially tested by the case where full inlining is not possible, which would also include a redundant binding for the
scrutinee if that issue were still present.
bishabosha added a commit that referenced this issue Apr 6, 2020
Fix #8306: Ensure the inliner can elide effectively pure case class applications in various situations
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
2 participants