Skip to content
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

Fix #17549: Unify how Memoize and Constructors decide what fields need storing. #17560

Merged
merged 1 commit into from
May 23, 2023

Conversation

sjrd
Copy link
Member

@sjrd sjrd commented May 23, 2023

The Memoize and Constructors have to work together and agree on which final vals actually need storing in a field. Previously, they used slightly different criteria: one on the result type, and one on the rhs (with an additional Scala.js-specific eligibility condition). That discrepancy resulted in the crash/wrong codegen in the issue.

We now unify both: we avoid a field if and only if all of the following apply:

  • it is a final val,
  • its result type is a ConstantType, and
  • it is eligible according to Scala.js semantics.

In particular, there is no condition on the right-hand-side. We avoid a field even if the right-hand-side has side effects. The side effects are moved to the constructor regardless.


This introduces both progressions and regressions in the amount of fields we generate. We can avoid fields even for side-effecting rhs'es, as long as the result type is constant. On the other hand, we now create a field for final vals with non-constant result type, even if the rhs is a literal.

While the latter is a regression for Scala 3, it aligns with the behavior of Scala 2. It also has the nice benefit that whether or not a val has a field is now independent of its implementation, and only dependent on its API. Overall, I think this is a trade-off worth taking.

We could reintroduce that optimization in the future (but in classes only; not in traits), if we really want to, although that would require dedicated code.

@@ -2,6 +2,6 @@ T.f1
T.f2
T.f3
T.f4
3 2 -3 -4
3 2 3 -4
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAICT, this is a progression.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems dubious to me because the result leaks before the side-effect runs. Maybe that never matters because leaking a more correct value is always better, given the semantics of initialization. But I had to convince myself.

@sjrd sjrd requested a review from smarter May 23, 2023 14:07
Comment on lines 264 to 287
def isConstExprFinalVal(using Context): Boolean =
atPhaseNoLater(erasurePhase) {
self.is(Final) && self.info.resultType.isInstanceOf[ConstantType]
self.is(Final, butNot = Mutable) && self.info.resultType.isInstanceOf[ConstantType]
} && !self.sjsNeedsField

def constExprFinalValConstantType(using Context): ConstantType =
atPhaseNoLater(erasurePhase) {
self.info.resultType.asInstanceOf[ConstantType]
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it could be expressed in a cleaner way using an extractor object?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. There is only 1 call site out of 5 that would benefit from the extractor variant. The other 4 would regress in readability.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, can you add a comment to constExprFinalValConstantType with something like @pre self.isConstExprFinalVal to make the pre-condition explicit?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, done.

@smarter smarter assigned sjrd and unassigned smarter May 23, 2023
…s need storing.

The Memoize and Constructors have to work together and agree on
which `final val`s actually need storing in a field. Previously,
they used slightly different criteria: one on the result type, and
one on the rhs (with an additional Scala.js-specific eligibility
condition). That discrepancy resulted in the crash/wrong codegen
in the issue.

We now unify both: we avoid a field if and only if all of the
following apply:

* it is a `final val`,
* its result *type* is a `ConstantType`, and
* it is eligible according to Scala.js semantics.

In particular, there is no condition on the right-hand-side. We
avoid a field even if the right-hand-side has side effects. The
side effects are moved to the constructor regardless.

---

This introduces both progressions and regressions in the amount of
fields we generate. We can avoid fields even for side-effecting
rhs'es, as long as the result type is constant. On the other hand,
we now create a field for `final val`s with non-constant result
type, even if the rhs is a literal.

While the latter is a regression for Scala 3, it aligns with the
behavior of Scala 2. It also has the nice benefit that whether or
not a val has a field is now independent of its *implementation*,
and only dependent on its *API*. Overall, I think this is a
trade-off worth taking.

We could reintroduce that optimization in the future (but in
classes only; not in traits), if we really want to, although that
would require dedicated code.
@smarter smarter enabled auto-merge May 23, 2023 15:18
@smarter smarter merged commit 3923a7d into scala:main May 23, 2023
@smarter smarter deleted the fix-codegen-final-vals branch May 23, 2023 16:34
@Kordyjan Kordyjan added this to the 3.3.1 milestone Aug 2, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants