-
Notifications
You must be signed in to change notification settings - Fork 8
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
Tail calls in Schism #5
Comments
I just wanted to chime in here with a link to a comment I posted in another thread about continuations which can be found here. It has a lot of overlap with this. In short, I'm working on an implementation of a compiler for Erlang, which leans heavily on continuations, implemented using a CPS-converted representation, so all calls are tail calls, and no function ever returns. Furthermore, the technique I'm using, Cheney on the MTA, handles the lack of tail calls in general by using This is not a technique unique to my compiler either: two very popular Scheme implementations use it as well, or some variant of it; Chicken Scheme and Chez; and I believe there are quite a few others that are less well-known that do as well. More importantly, a wide variety of functional languages could be implemented this way, as continuations provide an elegant foundation upon which to build exceptions/non-local returns, green threading, coroutines, backtracking (for the implementation of logic programming, e.g. Prolog), and more. Continuations depend heavily on optimized tail calls. My compiler is using LLVM, and on other platforms, I generally can't rely on, say, the From the perspective of a language implementer, even if WebAssembly gains something akin to LLVMs |
IIUC, there is nothing actionable left for the proposal. Please reopen concrete issues otherwise. |
I have a branch of Schism (a self-hosting Scheme-to-Wasm compiler) that makes use of the new
return_call
andreturn_call_indirect
instructions: https://github.com/eholk/schism/tree/tail-call-instrI thought I'd give some feedback on the proposal from the standpoint of a language implementation.
Adding tail call support to Schism
Adding the new opcodes was a pretty straightforward affair. I chose to do it by implementing a new pass,
replace-tail-calls
, that finds all calls in tail position with tail call instructions instead. The rest of the work was just forwarding those changes through the compiler until the encoding step where it outputs the right opcodes.The result validates in the spec interpreter, but I don't have a VM to run the output in yet. As time allows, I hope to modify the spec interpreter to implement Schism's runtime so that will be an option for running the output.
Schism's requirements from the tail call proposal
Scheme requires that an unbounded sequence of calls in tail position use a constant amount of stack space. All of Scheme's iteration facilities are expressed in terms of tail recursion (although some compilers recognize special cases and generate more traditional loops instead). Whereas in C and C++, tail recursion is an optimization the compiler may do, in Scheme tail recursion is not an optimization and is required for correctness.
So what does Scheme need from Wasm? Technically nothing. It's possible to generate a giant function with a big loop and a switch. It sounds like this is more or less what Go is doing. While this technically works, it's less than ideal. For example, it makes linking several Schism-compiled modules together impossible.
If Wasm has tail call instructions, Schism will happily use them. The most important thing for these instructions is that they are an honest-to-goodness tail call. For example, LLVM has the
tail
andmusttail
annotation, where thetail
annotation can be ignored. Schism needs the semantics ofmusttail
or else the instructions will be useless. Fortunately, this is the direction we're going with the proposal.The ideal case for Schism is that Wasm supports tail calls from any Wasm function to any Wasm function, including to imported functions that were exported from Wasm. This will make for the easiest implementation and give the most flexibility. As far as Wasm-to-JS calls, I think it is fine if these cannot be done as tail calls.
As far as whether may-tail-call or may-be-tail-called should be tracked in the type system, this doesn't particularly matter for Schism. All functions in Scheme need to be able to make tail calls and be tail called, so if extra type annotations are required, Schism will just use them unconditionally.
There was a suggestion of making the tail call annotation a per-module setting. In my mind, I would think of this as a section that provides requirements on the calling convention, similar to how LLVM supports different calling conventions. Basically, the new calling convention section would have the option to say "this module requires a calling convention that supports proper tail call handling." If we do this route, I think it's important to be a specced section, rather than a custom section like most of the hint section proposals have been. The reason is that custom sections are allowed to be completely ignored or even dropped (for example, you could imagine a CDN that drops custom sections on the fly to save bandwidth), whereas Schism needs to be able to rely on tail calls being tail calls.
Another suggestion was to only support tail calls with the caller and callee signature are the same (so-called natural tail calls, although from my perspective this is an unnatural restriction on tail calls). This is actually more workable for Schism than I thought at first. While I haven't settled on a design yet, there's a reasonable chance that all functions in Schism will have the same signature, due to the way Scheme does varargs. Still, even if all functions have the same signature, these would probably be done as a wrapper function with the standard signature that calls the real function with a more precise signature. Schism would then be able to directly call the precise function in a lot of cases. This optimization would not be possible if tail calls can only go from the same signature to the same signature. Basically, Schism could work with this restriction, but it would significantly limit what it's able to do.
Anyway, I'm excited to see Wasm get support for tail calls. Hopefully this perspective from at least one language implementation is helpful.
The text was updated successfully, but these errors were encountered: