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

guaranteed tail call elimination #81

Closed
wants to merge 1 commit into from

Conversation

Projects
None yet
10 participants
@thestinger
Copy link

thestinger commented May 19, 2014

No description provided.

@bstrie

This comment has been minimized.

Copy link
Contributor

bstrie commented May 19, 2014

When I asked @thestinger how this proposal deals with the widly-publicized email from a year ago explaining why Rust can't support TCE (https://mail.mozilla.org/pipermail/rust-dev/2013-April/003557.html), his response was along the lines of "LLVM supports this now, so everything is okay." But I'd still like to see the following point from Graydon's email elaborated upon in more detail:

Tail calls "play badly" with deterministic destruction. Including deterministic drop of ~ boxes. Not to say that they're not composable, but the options for composing them are UI-awkward, performance-penalizing, semantics-complicating or all of the above.

What does he mean by "play badly", and what implications does this have if this feature is accepted?

@cmr

This comment has been minimized.

Copy link
Member

cmr commented May 19, 2014

I am in favor of this, however should probably clarify what happens to locals. The way I envision it, locals will be destroyed before the tailcall.

@bstrie

This comment has been minimized.

Copy link
Contributor

bstrie commented May 19, 2014

Of course, I'm hugely in favor of this if we can pull it off. It promises to be a PR windfall as well.

@thestinger

This comment has been minimized.

Copy link
Author

thestinger commented May 19, 2014

@bstrie: The semantics are equivalent to return immediately after the call, so there's no way to destroy locals after the callee runs. Instead, locals could be destroyed before the call unless they are passed into it by-value but that's a bit weird.


This proposal adds an extra keyword to the language, although `be` has been reserved with this in
mind for some time. LLVM now provides this feature, but another backend may need to do extra manual
work to provide this as a guarantee.

This comment has been minimized.

@bstrie

bstrie May 19, 2014

Contributor

Not that I'm against accepting this feature merely out of concern for some hypothetical alternative implementation, but how much work are we talking here? Using GCC as an example, say.

This comment has been minimized.

@thestinger

thestinger May 19, 2014

Author

The commit adding x86 support for musttail was quite small. I don't know how much work this would involve for GCC. The feature is designed to be portable, but involves work for each new architecture.

This comment has been minimized.

@bstrie

bstrie May 19, 2014

Contributor

Thanks. Basically I just want to make sure that it's not so much work that our choices effectively boil down to "never implement Rust on any other backend" vs. "force all alternative implementations to completely ignore a language feature".

@bstrie

This comment has been minimized.

Copy link
Contributor

bstrie commented May 19, 2014

Re: using become rather than the long-reserved be, @thestinger's reply on IRC is that be gives no indication as to its meaning, whereas become implies "this function becomes that function". Personally I don't care either way, but become gives me an ineffable satisfaction due to having the same number of characters as return (though this could easily be construed as a mark against it as well).

* Locals with destructors could be permitted, and would be destroyed *before* the call when not
passing them to the callee.
* Passing non-immediate types as parameters is likely possible, but the current Rust calling
convention will get in the way.

This comment has been minimized.

@bstrie

bstrie May 19, 2014

Contributor

Can you elaborate on this last point, of our current calling convention getting in the way?

This comment has been minimized.

@thestinger

thestinger May 19, 2014

Author

The current convention is to perform a copy in the caller and pass a pointer to the alloca, which is clearly not allowed. AFAICT only passing large values by-value would work.

The current requirements proposed above are very strict and could be relaxed:

* The lint could be informed that a type like `Rc` will have a stable representation and
allow passing it to the callee.

This comment has been minimized.

@bstrie

bstrie May 19, 2014

Contributor

Do you mean by special-casing it into the compiler, or via an opt-in attribute? Because the former is a little gross, and the latter would be a dismayingly-specific thing for library authors to consider attaching to all of their types.

This comment has been minimized.

@thestinger

thestinger May 19, 2014

Author

Via an opt-in attribute for marking ABI stability.

This comment has been minimized.

@bstrie

bstrie May 19, 2014

Contributor

Could such an attribute have any uses beyond just allowing your type to play nicely with TCE?

This comment has been minimized.

@thestinger

thestinger May 19, 2014

Author

If Rust is ever going to support versioned ABIs where bugs can be fixed without breaking the ABI and recompiling every program linked against the library, something like that would be necessary.

This feature only just landed in LLVM, and Rust will need to leave this feature gated as the kinks
are worked out. The support is not completely solid on every platform yet, but it's designed to be a
portable feature and will produce an error if the requirements are not met or the platform support
is incomplete.

This comment has been minimized.

@bstrie

bstrie May 19, 2014

Contributor

What platforms specifically have solid support for this at the moment?

This comment has been minimized.

@thestinger

thestinger May 19, 2014

Author

Proper support for x86 was added on April 29th, other platforms haven't had the work done yet. It's a very new feature. That's why I'm proposing the introduction of this feature behind a feature gate.

This comment has been minimized.

@bstrie

bstrie May 19, 2014

Contributor

I'm somewhat afraid of us drumming up hype for this feature only to be left holding the bag if, six months from now, only x86 and x64 are supported with no ARM patches on the horizon. Having no idea what the development process for LLVM is like, is there any sort of concrete commitment to supporting this feature on less-popular architectures? In the worst-case scenario, would the semantics of become be such that we could gracefully degrade it into a return while "only" losing the promise that it won't blow the stack?

This comment has been minimized.

@thestinger

thestinger May 19, 2014

Author

I'm somewhat afraid of us drumming up hype for this feature only to be left holding the bag if, six months from now, only x86 and x64 are supported with no ARM patches on the horizon

It doesn't matter how long it takes for the feature to be finished in LLVM because it will be behind a feature gate until it's solid enough for Rust. If someone is upset about how long it's taking to finish, they can write the ARM code generation themselves.

Having no idea what the development process for LLVM is like, is there any sort of concrete commitment to supporting this feature on less-popular architectures?

The patch adding x86 / x86_64 support changed 95 lines. It's not like it would a substantial task to implement this for other architectures. It's part of the portable language specification so any target not implementing it can be considered incomplete and not supported upstream. You can consider the fact that it's a defined part of the LLVM language specification to be a strong commitment to implementing it.

In the worst-case scenario, would the semantics of become be such that we could gracefully degrade it into a return while "only" losing the promise that it won't blow the stack?

Degrading to a return would be an LLVM code generation bug. It will currently report a not implemented error for cases not yet correctly handled per the language specification. This isn't something Rust has to worry about, it simply remains feature gated until enough support is implemented.

The requirement of not accessing the caller's allocas is the hardest to enforce. The compiler would
forbid passing non-immediate types, and a default-deny lint would forbid passing non-primitive types
since the representation may change. The compiler would also forbid passing any lifetime-bounded
types not guaranteed to outlive the caller.

This comment has been minimized.

@bstrie

bstrie May 19, 2014

Contributor

I want to understand these better. Can you whip up a compile-fail example for each of the three restrictions in this paragraph, along with a short description of why the programs must be rejected? I feel like it's going to be very important to communicate our restrictions up-front, and this will help to that end.

@steveklabnik

This comment has been minimized.

Copy link
Member

steveklabnik commented May 19, 2014

I love become for reasons that don't really matter.

* The lint could be informed that a type like `Rc` will have a stable representation and
allow passing it to the callee.
* Locals with destructors could be permitted, and would be destroyed *before* the call when not
passing them to the callee.

This comment has been minimized.

@matthieu-m

matthieu-m May 19, 2014

This seems evil: imagine converting a return into a become and suddenly you get issues because the behavior changed ?

Instead, I would propose to make it visible: if you want to have locals that have a destructor: no problem, but wrap them in an explicit {} block so that it's visible they get destroyed before the become instruction.

This comment has been minimized.

@thestinger

thestinger May 19, 2014

Author

Well that's why it's in unresolved questions rather than part of the proposal. The restriction is painful, and it's unclear if there's a sane way of relaxing it.

This comment has been minimized.

@bstrie

bstrie May 21, 2014

Contributor

I recommend that we retain all of the restrictions for the purpose of this RFC. Seeing TCE get used in practice will give us data on which relaxations to pursue, and there's no rush since they're all totally backwards-compatible.

This comment has been minimized.

@dherman

dherman May 21, 2014

I don't think it's evil; because become is a control effect, as return is, it's explicit. But it's a different control effect and does things in different orders.

  • return: evaluate argument, then return
  • become: return, then evaluate argument

Edit: note that the alternative is even more of a refactoring hazard: if you simply can't do a become in the presence of destructors then non local changes break code.

@dherman

This comment has been minimized.

Copy link

dherman commented May 21, 2014

One thing to keep in mind: in languages with implicit proper tail calls, you can do convenient things like branch on two alternative calls in tail position:

if p() then { foo(a, b) } else { bar(x, y, z) }

Doing that kind of thing requires you to proliferate become keywords:

if p() then { become foo(a, b) } else { become bar(x, y, z) }

The thing is, I don't see a way to allow become to accept arbitrary expressions in its argument (as opposed to syntactically requiring a function call expression). You can't have the language statically reject function calls inside that expression that appear in non-tail position since e.g. you want to allow p() in

become (if p() then { foo(a, b) } else { bar(x, y, z) })

And frankly this defeats the purpose of having an explicit tail calling form in the first place; tail calls inside the body become implicit tail calls. So ISTM the only reasonable syntax is to require the argument of become to be a call.

Since we'd expect tail calls to be less common (and loops more common) in Rust than in languages like Scheme that rely on tail calls as the only form of iteration, it might be fine to have a bit of extra become noise. But I don't know if in practice tail-recursive algorithms might get unwieldy.

Probably that was the intention of the proposal (maybe it said so and I missed it?) but anyway I'm spelling out some consequences.

@matthieu-m

This comment has been minimized.

Copy link

matthieu-m commented May 22, 2014

I do not understand why become if p() { foo(a, b) } else { bar(x, y, z) }; could not be accepted.

In a way, I see become if p() { foo(a, b) } else { bar(x, y, z) }; as syntactic sugar for if p() { become foo(a, b); } else { become bar(x, y, z); } and I could see the Rust front-end "lowering" become easily either in if expressions or match expressions.

Of course, I would suggest starting without it, it is backward compatible anyway.

@thestinger

This comment has been minimized.

Copy link
Author

thestinger commented May 22, 2014

It would be quite confusing if become before an expression caused the calls later in the expression to destroy stuff earlier.

@bstrie

This comment has been minimized.

Copy link
Contributor

bstrie commented May 22, 2014

I must be missing something, how would it be possible to have both become foo(a, b) and become bar(x, y, z) in the same function? Isn't the function signature of the new function required to match that of the current function?

@thestinger

This comment has been minimized.

Copy link
Author

thestinger commented May 22, 2014

@bstrie: That's right, I didn't notice from the example.

@dherman

This comment has been minimized.

Copy link

dherman commented May 24, 2014

@bstrie: "Isn't the function signature of the new function required to match that of the current function?"

That's a shame. You can't make it work for arbitrary signatures (obviously with matching return type), with an appropriate amount of stack frame shuffling? Is this based on an LLVM limitation, a lack of desire to implement the shuffling, or a belief that the shuffling is "un-Rusty"? I understand if the latter but it is an unproven middle ground of expressiveness. It also needs to be spelled out just what constitutes matching signatures (eg a monomorphic function tail calling a compatible polymorphic function).

@dherman

This comment has been minimized.

Copy link

dherman commented May 24, 2014

@matthieu-m: The purpose of an explicit tail calling operator (become) in the first place, as distinct from just automatically making function calls in tail position behave as proper tail calls, is that the semantics of performing a function call as a tail call or not is observably different, and the performance model is radically different. If we didn't care about that we wouldn't design it as an explicit operator in the first place. But allowing function call nested inside a become expression to be implicit tail calls would be going right back to an implicit design. Only, it'd be a mucky middle ground, where some function calls are implicitly tail calls and others aren't. So I think the most sensible design is to make become syntactically restricted to take a call expression.

@thestinger

This comment has been minimized.

Copy link
Author

thestinger commented Jun 14, 2014

@dherman: The ABI has to match for musttail in order to make the cross-platform guarantee. LLVM had tail call optimization before musttail, but it wasn't reliable or portable. In Rust the sanest way to expose this is requiring the types to precisely match. It would be a severe backwards compatibility issue if adding a private field invalidated code using the type.

@alexcrichton

This comment has been minimized.

Copy link
Member

alexcrichton commented Jun 16, 2014

Closing, this was discussed a few weeks ago where we decided to postpone this.

@graue

This comment has been minimized.

Copy link

graue commented Oct 5, 2014

Updated discussion link for any other curious people reading this :)

@thestinger thestinger deleted the thestinger:become branch Nov 12, 2014

@mrhota

This comment has been minimized.

Copy link

mrhota commented Oct 10, 2016

updated link for the meeting minutes where the postponement was discussed: https://github.com/rust-lang/meeting-minutes/blob/master/weekly-meetings/2014-05-20.md

@petrochenkov

This comment has been minimized.

Copy link
Contributor

petrochenkov commented Feb 24, 2018

The next iteration - #1888 (also postponed).

@petrochenkov petrochenkov added T-lang and removed postponed labels Feb 24, 2018

@petrochenkov petrochenkov referenced this pull request Feb 24, 2018

Closed

Proper tail calls #1888

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.