Skip to content

Merge several IR nodes into UnaryOp. #5088

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

Merged
merged 2 commits into from
Dec 22, 2024
Merged

Conversation

sjrd
Copy link
Member

@sjrd sjrd commented Dec 15, 2024

ArrayLength, GetClass, Clone, IdentityHashCode, WrapAsThrowable, UnwrapFromThrowable and Throw are now ops of UnaryOp, rather than having their own IR nodes.

They all follow the general contract of how a UnaryOp must behave: first evaluate the argument, then perform an operation that only depends on the value of the argument (and not, for example, on the scope).

It makes the JS FunctionEmitter a bit messier. However, everything else gets simpler, as we get rid of dedicated paths for those 7 IR nodes.

In order to better fit with the other UnaryOps, notably the ones for jl.Class operations, we demand non-nullable arguments. An explicit CheckNotNull must be used for arguments that are nullable.


In this commit, we do not update the compiler yet. It still emits the old IR nodes, so that we can test the deserialiation hacks. That also means the case classes still exist for now.

@sjrd sjrd marked this pull request as ready for review December 15, 2024 14:21
@sjrd sjrd requested a review from gzm0 December 15, 2024 14:21
@gzm0
Copy link
Contributor

gzm0 commented Dec 15, 2024

FWIW: IMO JSDelete and JSImportCall can benefit from a similar treatment. Did you deliberately decide against these?

@sjrd
Copy link
Member Author

sjrd commented Dec 15, 2024

JSImportCall will need an additional argument once we add support for those with things. Also IMO it uses the scope. It's the module scope, not a function scope, but still. Its behavior is not only dependent on the argument.

For JSDelete, it's mostly because it would then be the only JS thing that we merge this way. I'd rather keep them separate. Also they have dubious behavior with evaluation order, and I don't want to pollute BinaryOp with that weirdness.

Copy link
Contributor

@gzm0 gzm0 left a comment

Choose a reason for hiding this comment

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

I'd appreciate separate commits for the review changes to ease re-review.


private def genWrapAsThrowable(tree: WrapAsThrowable): Type = {
val WrapAsThrowable(expr) = tree
private def genWrapAsThrowable(tree: UnaryOp): Type = {
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 to be inlined in genUnaryOp and unused otherwise. Remove?

}

tree.tpe

// scalastyle:on return
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider putting this after the early return at the beginning of the function.

IMO it will make it clearer to tell the story:

  1. attention, this function contains early returns
  2. code
  3. from here on, all early returns are done.
  4. more code

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh that's new. But it's a good strategy, indeed!

@@ -801,7 +798,7 @@ private final class ClassDefChecker(classDef: ClassDef,

checkApplyGeneric(method, args)

case UnaryOp(_, lhs) =>
case UnaryOp(op, lhs) =>
Copy link
Contributor

Choose a reason for hiding this comment

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

Change intended?

Copy link
Member Author

Choose a reason for hiding this comment

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

No. Reverted.

@@ -882,8 +882,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps:
case UnaryOp(UnaryOp.CheckNotNull, expr) =>
config.coreSpec.semantics.nullPointers == CheckedBehavior.Unchecked &&
isTriviallySideEffectFree(expr)
case GetClass(expr) => // Before 1.17, we used GetClass as CheckNotNull
config.coreSpec.semantics.nullPointers == CheckedBehavior.Unchecked &&
case UnaryOp(UnaryOp.GetClass, expr) => // Before 1.17, we used GetClass as CheckNotNull
Copy link
Contributor

Choose a reason for hiding this comment

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

If we need to keep this codepath anyways, how about:

Suggested change
case UnaryOp(UnaryOp.GetClass, expr) => // Before 1.17, we used GetClass as CheckNotNull
case UnaryOp(op, expr) if UnaryOp.isSideEffectFree(op) => // Before 1.17, we used GetClass as CheckNotNull

expandLongOps(foldUnaryOp(op, tlhs))(cont)
val folded = foldUnaryOp(op, tlhs)

folded match {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there an upside to folding the op first and then applying the checks for throwable wrapping? IIUC texpr below is always tlhs.

If not consider inverting to avoid confusion:

def folded = foldUnaryOp(op, tlhs)
op match {
  case UnaryOp.WrapAsThrowable =>
    if (...) else if (...) else folded
  // etc.
  case _ => expandLongOps(folded)(cont)
}

Copy link
Member Author

Choose a reason for hiding this comment

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

No strong opinion here. I changed it to your suggestion.

@@ -6920,7 +6880,6 @@ private[optimizer] object OptimizerCore {
case ApplyStatically(_, receiver, _, _, Nil) => isTrivialArg(receiver)
case ApplyStatic(_, _, _, Nil) => true

case ArrayLength(array) => isTrivialArg(array)
case ArraySelect(array, index) => isTrivialArg(array) && isTrivialArg(index)

case AsInstanceOf(inner, _) => isSimpleArg(inner)
Copy link
Contributor

Choose a reason for hiding this comment

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

Comment should be one line lower (but Github doesn't let me).

IIUC this will consider UnwrapFromThrowable (and others) a simple arg. Not sure we want that?

Copy link
Member Author

Choose a reason for hiding this comment

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

Arguably we might not want it for WrapAsThrowable and UnwrapAsThrowable, but those won't actually happen in practice. Even then, at call-site there's a good chance that they can be simplified with their expansion, so it might even be better anyway.

For the other ones, IMO we do want that. One I have seen in the diffs are throws. If a method does nothing but throw a simple arg, inlining it is probably beneficial.

I did not change this in this round, but I remain open to changing it if you have a stronger opinion.

ArrayLength(Null()),
ArraySelect(Throw(Null()), int(1))(NothingType),
ArrayLength(Throw(Null()))
UnaryOp(UnaryOp.Array_length, UnaryOp(UnaryOp.CheckNotNull, Null())),
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove this case? IMO it doesn't test really anything anymore.

I think if you want to add a test case / adjust this, we should check that Array_length now does not allow null anymore.

@@ -2066,7 +2066,7 @@ object Build {
if (!useMinifySizes) {
Some(ExpectedSizes(
fastLink = 449000 to 450000,
fullLink = 95000 to 96000,
fullLink = 94000 to 95000,
Copy link
Contributor

Choose a reason for hiding this comment

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

Heh, nice :) I guess less unnecessary null checks.

Copy link
Member Author

@sjrd sjrd left a comment

Choose a reason for hiding this comment

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

Updated with one commit addressing the review.

}

tree.tpe

// scalastyle:on return
Copy link
Member Author

Choose a reason for hiding this comment

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

Oh that's new. But it's a good strategy, indeed!

expandLongOps(foldUnaryOp(op, tlhs))(cont)
val folded = foldUnaryOp(op, tlhs)

folded match {
Copy link
Member Author

Choose a reason for hiding this comment

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

No strong opinion here. I changed it to your suggestion.

@@ -6920,7 +6880,6 @@ private[optimizer] object OptimizerCore {
case ApplyStatically(_, receiver, _, _, Nil) => isTrivialArg(receiver)
case ApplyStatic(_, _, _, Nil) => true

case ArrayLength(array) => isTrivialArg(array)
case ArraySelect(array, index) => isTrivialArg(array) && isTrivialArg(index)

case AsInstanceOf(inner, _) => isSimpleArg(inner)
Copy link
Member Author

Choose a reason for hiding this comment

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

Arguably we might not want it for WrapAsThrowable and UnwrapAsThrowable, but those won't actually happen in practice. Even then, at call-site there's a good chance that they can be simplified with their expansion, so it might even be better anyway.

For the other ones, IMO we do want that. One I have seen in the diffs are throws. If a method does nothing but throw a simple arg, inlining it is probably beneficial.

I did not change this in this round, but I remain open to changing it if you have a stronger opinion.

@@ -801,7 +798,7 @@ private final class ClassDefChecker(classDef: ClassDef,

checkApplyGeneric(method, args)

case UnaryOp(_, lhs) =>
case UnaryOp(op, lhs) =>
Copy link
Member Author

Choose a reason for hiding this comment

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

No. Reverted.

@sjrd sjrd requested a review from gzm0 December 22, 2024 12:23
Copy link
Contributor

@gzm0 gzm0 left a comment

Choose a reason for hiding this comment

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

LGTM (after squashing obviously).

sjrd added 2 commits December 22, 2024 15:26
`ArrayLength`, `GetClass`, `Clone`, `IdentityHashCode`,
`WrapAsThrowable`, `UnwrapFromThrowable` and `Throw` are now ops
of `UnaryOp`, rather than having their own IR nodes.

They all follow the general contract of how a `UnaryOp` must behave:
first evaluate the argument, then perform an operation that only
depends on the value of the argument (and not, for example, on the
scope).

It makes the JS `FunctionEmitter` a bit messier. However,
everything else gets simpler, as we get rid of dedicated paths for
those 7 IR nodes.

In order to better fit with the other `UnaryOp`s, notably the ones
for `jl.Class` operations, we demand non-nullable arguments. An
explicit `CheckNotNull` must be used for arguments that are
nullable.

---

In this commit, we do not update the compiler yet. It still emits
the old IR nodes, so that we can test the deserialiation hacks.
That also means the case classes still exist for now.
And update the compiler to directly generate the new `UnaryOp`s.

We only enable the deserialization hack when reading IR <= 1.17.
@sjrd
Copy link
Member Author

sjrd commented Dec 22, 2024

(rebased to address conflicts in BinaryIncompatibilities)

@sjrd sjrd merged commit f82e59f into scala-js:main Dec 22, 2024
3 checks passed
@sjrd sjrd deleted the more-unary-ops branch December 22, 2024 21:48
sjrd added a commit to dotty-staging/dotty that referenced this pull request Jan 17, 2025
And port the changes that were made to the compiler backend.
Most nobably, the changes from the following upstream PRs:

* Introduce non-nullable reference types in the IR.
  scala-js/scala-js#5018
* Opt: Remove useless *Ident indirections in the IR model.
  scala-js/scala-js#5092
* Merge the IR node This into VarRef, with a magic LocalName.
  scala-js/scala-js#5090
* Merge several IR nodes into UnaryOp.
  scala-js/scala-js#5088
* Restrict usages of StoreModule even further.
  scala-js/scala-js#5059
* Allow Return arg to be a void if the target Labeled is a void.
  scala-js/scala-js#5074
* Rename NoType to VoidType and print it as "void".
  scala-js/scala-js#5061
sjrd added a commit to dotty-staging/dotty that referenced this pull request Jan 17, 2025
And port the changes that were made to the compiler backend.
Most notably, the changes from the following upstream PRs:

* Introduce non-nullable reference types in the IR.
  scala-js/scala-js#5018
* Opt: Remove useless *Ident indirections in the IR model.
  scala-js/scala-js#5092
* Merge the IR node This into VarRef, with a magic LocalName.
  scala-js/scala-js#5090
* Merge several IR nodes into UnaryOp.
  scala-js/scala-js#5088
* Restrict usages of StoreModule even further.
  scala-js/scala-js#5059
* Allow Return arg to be a void if the target Labeled is a void.
  scala-js/scala-js#5074
* Rename NoType to VoidType and print it as "void".
  scala-js/scala-js#5061
sjrd added a commit to scala/scala3 that referenced this pull request Jan 22, 2025
And port the changes that were made to the compiler backend.
Most notably, the changes from the following upstream PRs:

* scala-js/scala-js#5018
* scala-js/scala-js#5092
* scala-js/scala-js#5090
* scala-js/scala-js#5088
* scala-js/scala-js#5059
* scala-js/scala-js#5074
* scala-js/scala-js#5061
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.

2 participants