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

Explicit Tail Calls #3407

Open
wants to merge 92 commits into
base: master
Choose a base branch
from
Open

Explicit Tail Calls #3407

wants to merge 92 commits into from

Conversation

phi-go
Copy link

@phi-go phi-go commented Apr 6, 2023

This RFC proposes a feature to provide a guarantee that function calls are tail-call eliminated via the become keyword. If this guarantee can not be provided an error is generated instead.

Rendered

For reference, previous RFCs #81 and #1888, as well as an earlier issue #271, and the currently active issue #2691.

Rust tracking issue: rust-lang/rust#112788

text/0000-guaranteed-tco.md Outdated Show resolved Hide resolved
text/0000-guaranteed-tco.md Outdated Show resolved Hide resolved
text/0000-guaranteed-tco.md Outdated Show resolved Hide resolved
text/0000-guaranteed-tco.md Outdated Show resolved Hide resolved
@ehuss ehuss added the T-lang Relevant to the language team, which will review and decide on the RFC. label Apr 6, 2023
@Robbepop
Copy link
Contributor

Robbepop commented Apr 6, 2023

thanks a ton @phi-go for all the work you put into writing the RFC so far! Really appreciated! 🎉

text/0000-guaranteed-tco.md Outdated Show resolved Hide resolved
text/0000-guaranteed-tco.md Outdated Show resolved Hide resolved
text/0000-guaranteed-tco.md Outdated Show resolved Hide resolved
text/0000-guaranteed-tco.md Outdated Show resolved Hide resolved
@clarfonthey
Copy link
Contributor

While I personally know the abbreviation TCO, I think that it would be helpful to expand the acronym in the issue title for folks who might not know it at first glance.

@phi-go phi-go changed the title Guaranteed TCO Guaranteed TCO (tail call optimization) Apr 6, 2023
text/0000-guaranteed-tco.md Outdated Show resolved Hide resolved
text/0000-guaranteed-tco.md Outdated Show resolved Hide resolved
text/0000-guaranteed-tco.md Outdated Show resolved Hide resolved
text/0000-guaranteed-tco.md Outdated Show resolved Hide resolved
text/0000-guaranteed-tco.md Outdated Show resolved Hide resolved
text/0000-guaranteed-tco.md Outdated Show resolved Hide resolved
text/0000-guaranteed-tco.md Outdated Show resolved Hide resolved
text/0000-guaranteed-tco.md Outdated Show resolved Hide resolved
text/0000-guaranteed-tco.md Outdated Show resolved Hide resolved
@VitWW
Copy link

VitWW commented Apr 6, 2023

An alternative is to mark a function (like in OCaml), not a return keyword:

recursive fn x() {
    let a = Box::new(());
    let b = Box::new(());
    y(a);
}

@Robbepop
Copy link
Contributor

Robbepop commented Apr 6, 2023

An alternative is to mark a function (like in OCaml), not a return keyword:

recursive fn x() {
    let a = Box::new(());
    let b = Box::new(());
    y(a);
}

The RFC already highlights an alternative design with markers on function declarations and states that tail calls are a property of the function call and not a property of a function declaration since there are use cases where the same function is used in a normal call and a tail call.

@digama0
Copy link
Contributor

digama0 commented Apr 6, 2023

Note: this may be suitable either as a comment on #2691 or here. I'm assuming interested parties are watching both anyway.

The restriction on caller and callee having the exact same signature sounds quite restrictive in practice. Comparing it with [Pre-RFC] Safe goto with value (which does a similar thing to the become keyword but for intra-function control flow instead of across functions), the reason that proposal doesn't have any requirements on labels having the same arguments is because all parameters in all labels are part of the same call frame, and when a local is used by different label than the current one it is still there in memory, just uninitialized.

If we translate it to the become approach, that basically means that each function involved in a mutual recursion would (internally) take the union of all the parameters of all of the functions, and any parameters not used by the current function would just be uninitialized. There are two limitations to this approach:

  • It changes the calling convention of the function (since you have to pad out the arguments with these uninitialized locals)
  • The calling convention depends on what other functions are called in the body

I don't think this should be handled by an actual calling convention label though. Calling these "rusttail" functions for discussion purposes, we have the following limitations:

  • You can't get a function pointer with "rusttail" convention
  • You can't use a "rusttail" function cross-crate

The user doesn't have to see either of these limitations directly though; the compiler can just generate a shim with the usual (or specified) calling convention which calls the "rusttail" function if required. Instead, they just observe the following restrictions, which are strictly weaker than the one in the RFC:

  • If you become a function in another crate, the arguments of caller and callee have to match exactly
  • If you only become a function in the same crate, there are no restrictions
  • (If f tail-calls a cross-crate function g and also an internal function h, then f,g,h must all have the same arguments)

text/0000-guaranteed-tco.md Outdated Show resolved Hide resolved
@WaffleLapkin
Copy link
Member

@digama0 the extern "rust-tail" fn proposal is similar to your "same-crate" one, in that it allows more code in some cases. I.e. I can imagine the end-game requirement of (musttail) become be something like

become f(...) requires at least one of the following:

  • Caller and callee function signatures match exactly (modulo lifetimes)
  • Caller and callee are defined in the same crate
  • Caller and callee both use "rust-tail" calling convention

I'll try to implement those relaxations in the experiment (once the things described in the RFC right now are fully implemented and merged...), but I also want to highlight @phi-go's mention that those relaxations can be RFC-ed separately from this RFC (and they probably should, to keep the scope smaller!).

@joshtriplett
Copy link
Member

@Robbepop Using ? with that meaning seems like a non-starter given Rust's existing usage of ?.

@joshtriplett joshtriplett removed the I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. label Aug 1, 2023
@joshtriplett
Copy link
Member

Un-nominating the RFC for now.

To be explicitly clear on next steps: 👍 for going ahead with one or more experiments (including a tail-call with placeholder keyword, and a tail call calling-convention as @scottmcm suggested), blocking concerns for an experiment or RFC that uses the demo-looks-done approach of become (not dead-set against it but very much currently feeling there are better alternatives).

@Robbepop
Copy link
Contributor

Robbepop commented Aug 10, 2023

@Robbepop Using ? with that meaning seems like a non-starter given Rust's existing usage of ?.

I wouldn't say it is a non-starter. The same reasoning could have been applied to the highly debated .await syntax were .something only referred to field accesses before. I know why it isn't good to introduce "new" syntax but sometimes it takes courage to try something new. And new doesn't always mean it is a bad thing.

blocking concerns for an experiment or RFC that uses the demo-looks-done approach of become (not dead-set against it but very much currently feeling there are better alternatives).

If during all the time of the RFC thread I'd have seen a single alternative syntax that was actually cutting it, I'd wholeheartedly agree. Yet, we do not even has consensus what an agreeable placeholder syntax could look like. A bit of direction from the people who are deciding over nomination could probably help here.

In light of the RFC's un-nomination I would like to know what the status of the work behind the RFC is as this RFC thread has been very silent since quite a few weeks now. What in particular is the stance of @WaffleLapkin (RFC implementer) and @phi-go (RFC author) about the next steps provided by @joshtriplett ?

I personally just hope that this RFC won't die due to too much bikeshedding. It would be really sad to lose the momentum that has been built up for this long awaited Rust feature.

@phi-go
Copy link
Author

phi-go commented Aug 10, 2023

To my understanding we are just waiting for the implementation of the experiment. Here is the tracking issue: rust-lang/rust#112788. So un-nominating until the implementation is done seems fine to me.

Regarding the placeholder syntax, we are indeed waiting on a decision for the actual syntax. The current state is that the current implementation uses become and it seems non-trivial to change but @WaffleLapkin will know better.

I find become? quite intuitive, so I would have hoped for some contemplation. The only other candidate syntaxes that have been discussed for longer are using an attribute, and the return variation.

bors added a commit to rust-lang/rust-analyzer that referenced this pull request Feb 14, 2024
feature: Add basic support for `become` expr/tail calls

This follows rust-lang/rfcs#3407 and my WIP implementation in the compiler.

Notice that I haven't even *opened* a compiler PR (although I plan to soon), so this feature doesn't really exist outside of my WIP branches. I've used this to help me test my implementation; opening a PR before I forget.

(feel free to ignore this for now, given all of the above)
@lolbinarycat
Copy link
Contributor

the rfc does not link to the tracking issue

@phi-go
Copy link
Author

phi-go commented Sep 28, 2024

@lolbinarycat Looking at other RFCs, the tracking issue does not seem to be included. A link is in the first comment here anyway, so seems fine to me.

@lolbinarycat
Copy link
Contributor

a lot of other RFCs don't have tracking issues.

it's supposed to go under the Rust Issue: #0000 bit, replacing the placeholder 0000.

@phi-go
Copy link
Author

phi-go commented Sep 28, 2024

I see, updated. I thought those should be filled in when the RFC is accepted.

@scottmcm
Copy link
Member

scottmcm commented Oct 1, 2024

I stumbled on this earlier: https://github.com/bytecodealliance/rfcs/blob/main/accepted/pulley.md

The wasmtime folks sound interested in using become for an interpreter.

@fitzgen
Copy link
Member

fitzgen commented Oct 2, 2024

The wasmtime folks sound interested in using become for an interpreter.

Indeed we are! We even have an open PR that is just about ready to merge and adds a switch to turn become usage on/off: bytecodealliance/wasmtime#9251

@traviscross
Copy link
Contributor

This came up multiple times, with positive affection, in recent extended lang meetings. And since then, I've become aware of an intriguing use case for this that involves interoperability with Rust and our ecosystem. Seemingly it is on our minds.

text/0000-explicit-tail-calls.md Outdated Show resolved Hide resolved
text/0000-explicit-tail-calls.md Outdated Show resolved Hide resolved
@Rudxain
Copy link

Rudxain commented Nov 5, 2024

According to GH search algorithm, this thread doesn't contain any instance of "successor" or "combinator". I'm somewhat surprised.

I've been experimenting with those, both in Rust and TypeScript (the link is mostly TS, but I have repos where I use successors relatively extensively)

I assume the combinator approach is more efficient than a trampoline.

successors and the combinator are equivalent. The only diff is that the combinator applies a reduction (folding) that selects a "slice" of the final state, instead of yielding all the internal states (like a normal iterator would do). So a combinator is useful for hiding implementation details, and successors is useful for preserving information (reversibility?)

IMO, the RFC should also compare the pros and cons of using those alternatives. The first one that comes to mind is that become can be trivially used in a const fn, but successors cannot (const traits aren't stable yet)

phi-go and others added 2 commits November 5, 2024 17:16
Co-authored-by: Ricardo Fernández Serrata <[email protected]>
Co-authored-by: Ricardo Fernández Serrata <[email protected]>
@phi-go
Copy link
Author

phi-go commented Nov 5, 2024

Thanks for taking a look! I have to say I'm not really familiar with successors and combinators, they didn't come up during the discussion as you already noticed.

Looking at those they seem very interesting. At least if one is not doing general tail recursion, which would also be a con as to why they cannot replace explicit tail calls, if I understood that right. I will probably have time to add a section on those this weekend.

@phi-go
Copy link
Author

phi-go commented Nov 10, 2024

@Rudxain I added a section, if you have time I would appreciate if you could take a look. I left out the part regarding const as it is not really a fundamental problem and could change in the future.

text/0000-explicit-tail-calls.md Outdated Show resolved Hide resolved
text/0000-explicit-tail-calls.md Outdated Show resolved Hide resolved

### Principled Local Goto
One alternative would be to support some kind of local goto natively, indeed there exists a
[pre-RFC](https://internals.rust-lang.org/t/pre-rfc-safe-goto-with-value/14470/9?u=scottmcm) ([comment](https://github.com/rust-lang/rfcs/issues/2691#issuecomment-1458604986)) or another (LLVM specific idea)[https://internals.rust-lang.org/t/idea-for-safe-computed-goto-using-enums/21787?u=programmerjake]. These designs should be able to achieve the targeted performance and stack usage, though they seems to be quite difficult to implement or are backend specific. Also they do not seem to be as flexible as the chosen design, especially implementing tail calls to indirect or external functions do not seem to be feasible.
Copy link
Member

Choose a reason for hiding this comment

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

Note that the computed goto idea I proposed isn't LLVM specific, since computed goto originally was a gcc extension for C/C++ before LLVM started, so it should work just fine in gcc too.

Copy link
Member

Choose a reason for hiding this comment

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

imo computed goto is more useful than tail calls in some situations (e.g. better register allocation due to not being restricted by the function call ABI) and less useful in others, so I think both tail calls and computed goto are worth adding to rust.

Copy link

@rpjohnst rpjohnst Nov 10, 2024

Choose a reason for hiding this comment

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

In principle there's nothing preventing an optimizer from doing the same sort register allocation with tail calls, if the callees are equivalently local to the goto targets.

And on the other hand, in practice tail calls using a fixed ABI can produce better register allocation than computed goto, because a big irreducible CFG can trip up the allocator's heuristics in ways that separate functions can be written to avoid.

The better argument for computed goto is to unlock intraprocedural borrow checking, but even then it should probably look more like a tail call to a local function (with parameters), analogous to break-with-value, and less like the C extension, which doesn't play well with block scope or destructors.

Choose a reason for hiding this comment

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

What about the following Lisp-inspired syntax?

tagbody! {
   'a: {  /* do some work */ goto 'b; }
   'b: { /* do some work */ 1 }
}

Copy link
Member

Choose a reason for hiding this comment

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

What about the following Lisp-inspired syntax?

tagbody! {
   'a: {  /* do some work */ goto 'b; }
   'b: { /* do some work */ 1 }
}

the syntax I had proposed is essentially equivalent to that.

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Author

Choose a reason for hiding this comment

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

@programmerjake Would you agree that local gotos require backend support? Then I would update that section to mention that instead.

I would also add some text that local gotos could have other applications than for tail calls. While this seems to already be disputed I guess we do not really know until this is tried?

Copy link
Member

Choose a reason for hiding this comment

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

@programmerjake Would you agree that local gotos require backend support?

yes, assuming you meant computed goto (jumping to an address in a variable instead of to a known label), but afaik all targets on gcc and/or llvm already support computed goto -- the main issue i raised in that internals thread is LLVM doesn't have a way to refer to a label's address from another codegen unit, so that needs to be fixed or worked around if we're using enums and not just a raw pointer to hold the addresses of our labels.

Then I would update that section to mention that instead.

ok

I would also add some text that local gotos could have other applications than for tail calls.

computed gotos do not let you make tail calls, they only work within a function instead of calling between functions. they can just be used for similar applications such as making fast interpreters.

gotos in general are also useful for expressing irreducible control flow, if we got computed goto, it can also be used as a normal goto to express that control flow.

Copy link
Author

Choose a reason for hiding this comment

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

yes, assuming you meant computed goto (jumping to an address in a variable instead of to a known label), but afaik all targets on gcc and/or llvm already support computed goto -- the main issue i raised in that internals thread is LLVM doesn't have a way to refer to a label's address from another codegen unit, so that needs to be fixed or worked around if we're using enums and not just a raw pointer to hold the addresses of our labels.

Ah, thanks for the clarification. So for a known label this is in theory possible without backend support?

computed gotos do not let you make tail calls, they only work within a function instead of calling between functions. they can just be used for similar applications such as making fast interpreters.

Yeah, sorry I meant other use cases than being a possible alternative to the functionality provided by tail calls.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, thanks for the clarification. So for a known label this is in theory possible without backend support?

Yes, MIR is a CFG already. Jumping to a known label uses the exact same MIR block terminator as is used for eg control flow converging back to the same code path after an if else. Only the MIR building code and steps before it would need to be updated.

phi-go and others added 2 commits November 11, 2024 09:27
Co-authored-by: Ricardo Fernández Serrata <[email protected]>
Co-authored-by: Ricardo Fernández Serrata <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.