8000 Make the label of `Return` nodes mandatory. by sjrd · Pull Request #3323 · scala-js/scala-js · GitHub
[go: up one dir, main page]

Skip to content

Make the label of Return nodes mandatory. #3323

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 1 commit into from
Apr 4, 2018

Conversation

sjrd
Copy link
Member
@sjrd sjrd commented Mar 31, 2018

This builds on top of #3322. It will need a rebase, unless we decide to merge both as one in this PR.

A Return with a None label implicitly meant returning from the enclosing Closure or MethodDef. In the spirit of solving all problems with Labeled blocks, we can get rid of this special-case by introducing an explicit function-wide Labeled block when necessary.

Instead of emitting:

def foo__I(): int = {
  ...
  if (c)
    return 5
  ...
}

we would now use

def foo__I(): int = {
  _return[int]: {
    ...
    if (c)
      return@_return 5
    ...
  }
}

which is equivalent.

This complicates somewhat the compiler back-end, since it now needs to take care of adding that label if necessary (it could always add one, but that would not be nice, and defeat most of the purpose of this change), and it does not really simplify anything later in the pipeline either.

Moreover, we observe a slight regression of quality of generated .js code in some rare cases: when there is a return in a void-returning method, and that method is not inlined, the FunctionEmitter cannot replace the break _return by a return in JS. This is due to a technicality in FunctionEmitter, and this restriction could be lifted in the future.

The main benefits are more regular IR, as well as a potential performance improvement in the optimizer. Indeed, previously, every time we inlined a method, we needed to preemptively summon a lot of complicated machinery to introduce a label in case the body contains a return. In most cases, it doesn't, and we detect after the fact that the label is useless and can be removed. Instead, we can now straightforwardly process the body without any extra action. In the rare cases where the body did contain a return, the optimizer will encounter the Labeled block, and spawn the machinery then. In all the other cases, the machinery is completely by-passed.

@sjrd sjrd requested a review from gzm0 March 31, 2018 17:45
@sjrd
Copy link
Member Author
sjrd commented Mar 31, 2018

After #3322, I felt compelled to try and remove the last asymmetry between Labeled and Return, which were supposed to work together but Return had 1 more function, which was not nice.

Anecdotally, this removes the last Option in all the Trees, i.e., all the statement/expression nodes (not all the IRNodes).

@sjrd sjrd force-pushed the no-return-without-label branch 2 times, most recently from 5093f28 to dcece86 Compare April 2, 2018 18:51
@sjrd sjrd changed the title DO NOT MERGE Make the label of Return nodes mandatory. Make the label of Return nodes mandatory. Apr 2, 2018
@sjrd
Copy link
Member Author
sjrd commented Apr 2, 2018

Rebased.

A `Return` with a `None` label implicitly meant returning from the
enclosing `Closure` or `MethodDef`. In the spirit of solving all
problems with `Labeled` blocks, we can get rid of this special-case
by introducing an explicit function-wide `Labeled` block when
necessary.

Instead of emitting:

    def foo__I(): int = {
      ...
      if (c)
        return 5
      ...
    }

we would now use

    def foo__I(): int = {
      _return[int]: {
        ...
        if (c)
          return@_return 5
        ...
      }
    }

which is equivalent.

This complicates somewhat the compiler back-end, since it now needs
to take care of adding that label if necessary (it could *always*
add one, but that would not be nice, and defeat most of the
purpose of this change), and it does not really simplify anything
later in the pipeline either.

Moreover, we observe a slight regression of quality of generated
.js code in some rare cases: when there is a `return` in a
`void`-returning method, and that method is not inlined, the
`FunctionEmitter` cannot replace the `break _return` by a `return`
in JS. This is due to a technicality in `FunctionEmitter`, and this
restriction could be lifted in the future.

The main benefits are more regular IR, as well as a potential
performance improvement in the optimizer. Indeed, previously, every
time we inlined a method, we needed to preemptively summon a lot of
complicated machinery to introduce a label in case the body
contains a `return`. In most cases, it doesn't, and we detect after
the fact that the label is useless and can be removed. Instead, we
can now straightforwardly process the body without any extra
action. In the rare cases where the body did contain a `return`,
the optimizer will encounter the `Labeled` block, and spawn the
machinery then. In all the other cases, the machinery is completely
by-passed.
@sjrd sjrd f 8000 orce-pushed the no-return-without-label branch from dcece86 to c61d78d Compare April 3, 2018 11:11
@gzm0
Copy link
Contributor
gzm0 commented Apr 3, 2018

I like this.

Moreover, we observe a slight regression of quality of generated .js code in some rare cases: when there is a return in a void-returning method, and that method is not inlined, the FunctionEmitter cannot replace the break _return by a return in JS. This is due to a technicality in FunctionEmitter, and this restriction could be lifted in the future.

Could you go a bit more into details here? I tried figuring this out, but couldn't (by looking at emitter).

@sjrd
Copy link
Member Author
sjrd commented Apr 3, 2018

Basically, if we have a void-returning method, we now have

def foo__I__V(x: int) {
  ret[void]: {
    if (x < 0)
      return@ret (void 0)
    println(x)
  }
}

When we emit this method, because it is a void method, we transform its body with transformStat. Now, transformStat on the Labeled delegates to pushLhsInto with a Lhs.Discard. This means that now the label ret will be associated with a Lhs.Discard, whereas previously, the implicit top-level label would have been associated with (what we now call) a Lhs.ReturnFromFunction. When encountering the return@ret (void 0), the Lhs.Discard means that we emit (void 0); break ret; (the (void 0) disappears since it does not have side effects), whereas previously we would directly emit return (void 0).

We could avoid this if, at the top-level of a def, we hijacked a top-level Labeled block to use Lhs.ReturnFromFunction instead of Lhs.Discard (in a similar way that a While loop hijacks a top-level Labeled inside its body to turn it into a continue).

The reason it works better for non-void methods is that we transform the body using pushLhsInto(Lhs.ReturnFromFunction, body), which means that the treatment of the Labeled associates Lhs.ReturnFromFunction to its label, and hence the return@ret sees it. We cannot do the same thing with a void-returning method, because it is not valid to call pushLhsInto(lhs, rhs) with an lhs that is not Lhs.Discard when the rhs has type NoType (since it is a statement).

Is that clear?

@@ -477,13 +473,17 @@ private[emitter] class FunctionEmitter(jsGen: JSGen) {
val newParams =
(if (translateRestParam) params.init else params).map(transformParamDef)

val newBody = transformStat(withReturn, Set.empty)(env) match {
val newBody =
if (isStat) transformStat(body, Set.empty)(env)
Copy link
Contributor

Choose a reason for hiding this comment

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

So basically this line would have to hijack the labeled block. Fair enough.

@gzm0 gzm0 merged commit d592b99 into scala-js:master Apr 4, 2018
@sjrd sjrd deleted the no-return-without-label branch April 4, 2018 05:21
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
0