Join GitHub today
GitHub is home to over 40 million developers working together to host and review code, manage projects, and build software together.Sign up
[hail] tail-recursive loops #7614
I don't think I'm quite satisfied with this implementation, although I think that it will do the things we need it to do, for the most part. It mostly adheres to the structure that @chrisvittal was implementing in #5228, although I had some questions/notes about implementation/interface and would love some input:
but I'm not sure I really like any of them.
Awesome! I will take a closer look, but here's my quick reaction to the interface. Was I was hoping to write was:
This is basically modeled after letrec in lisp/scheme/ml which would look something like:
Another option is to get rid of
where the first argument to the lambda is the loop itself.
I think main problem with your proposals (except maybe 3) is that it assumes too much about the structure of the loop: namely, it embeds the exit condition into the structure of the loop. The loop may have several backwards calls or exit points (e.g. in a case or if tree) and there maybe shared and conditional work that happens inside that tree (and even nested loops!)
@cseed I think my point is that I want an interface that assumes the structure of the loop. I'm happy to implement other interfaces as well--the one that I've currently got in this PR is basically your second suggestion--but I think it would also be nice to have a while loop construct that looks syntactically more similar to what you'd expect a while loop to look like in python, if that makes sense?
So I'm going to insist on the classical loop interface I described above, since it is strictly more powerful than the interfaces you've proposed. I don't have a strong feeling if you want to also add a Python-inspired while loop (although I personally would find the similarities misleading given the required differences, I understand others might feel differently). Your while loop should be naturally implementable in terms of mine, so I also suggest we focus on that first.
Giving each loop a name seems natural. Apart from the wrapping issue (the greatest existential threat our generation faces) I don't see any problem calling an outer loop from an inner loop.
Is Patrick's proposal for extra types written up anywhere? I don't like the idea of complicating the type hierarchy for internal bookkeeping like this.
So I'm going to remark that in the code generator it is often natural to build data structures to aid the organization of the code generator, and those data structures need not need to be types/IRs. Given that Recur has to be in tail position, and you know exactly when you're existing the loop (branches that don't contain recur nodes). So the compilation looks like:
What I would do is "peel" off the ifs and lets (anything else?) that can sit in tail position and build a separate data structure for those nodes which I then traverse to emit the above code.
Using the stream interface seems wrong to me also. What's the type of the stream the loop turns into? Since loops carry multiple values (by design), memory allocating these to create a tuple stream is going to be a performance non-starter.
I'll comment more once I've looked over the code.
I agree that the tail-recursion interface seems like the right primitive to expose in python, on top of which we could implement convenience methods for building while/for loops if we decide it's worth it.
Also agree. This will require either adding another context of loops/continuations in the environment (valid places to jump to, and their argument types), or keeping them in the normal value context by adding a new continuation type.
My proposal has two main differences. In
the point that jumps back to the top of the loop is explicit, but the point that jumps out of the loop is not. I suggested making this something like
or, if we're giving names to loops, it might be simpler to pass the break and recur functions to the lambda:
The second difference is in the typing. In this PR, the
In the type checker,
One nice property of this setup is that if an expression has a non-bottom type, then it is guaranteed not to jump away from itself (it may jump internally), so it is safe to method-wrap.
This also make codegen very simple, and @iitalics has already implemented it! See
In the IR, I don't think this requires much change. If we're already adding a continuation context (as mentioned above), then
There's also a middle ground where we make break continuations explicit in the IR, but we want to keep the scheme-like interface in python. Then the pass @cseed described for inferring where the loop exits are would just go in python instead of the emitter.
When I mentioned this, I was thinking we could reuse the region management logic from the stream emitter for loops, but I've since changed my mind. I think loops will have hard region management no matter what.
As a side note, @iitalics stream emitter can handle streams of multiple values fine. Effectively, you can make a
This proposal mounts to programming with explicit continuations. It doesn't increase the expressiveness of the loop construct that I can see. Our users are reluctant enough to learn functional programming, I think continuations is one step too far for the user interface. Internally, I don't care as much, although personally I would prefer to code up my solution. @catoverdrive's doing the work, so I'll let them decide.
Ah, I thought @catoverdrive was referring to IR level streams.
ok, great. @cseed I've essentially already implemented the thing that you've described (although I need to actually duplicate the IR nodes in python for the loop function to work, and add some tests), although I haven't peeled off the ifs and lets to emit separately (and I don't know if we need to? seems to work fine with the current code generator); we check that all recurs are in tail position when we typecheck the IR. I will clean up the python and give the loops names and then assign this to someone.