-
Notifications
You must be signed in to change notification settings - Fork 571
Support TCO for functions with tail-recursive inner functions #3958
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
Conversation
garyb
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! Touching anything in here is a little tricky, but as far as I can tell it all looks sensible. And there's the tests, of course. 🙂
|
Are there any tests we should add to this? Or does this seem good to merge? |
|
I've added a small fixup which addresses a test case I hadn't sufficiently thought through originally. |
hdgarrood
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd be a bit more comfortable with this if the tests were exercising that the accumulator is being passed around correctly e.g. by eventually returning a value which depends on the number of recursive calls made. If the TCO variables weren't being assigned properly somehow (if we were somehow immediately reverting to the base case, say), I think these tests might not catch that. However I'll leave that up to you; I'm happy for you to go ahead and merge this now too.
|
One of the tests does depend non-trivially on the recursion being correct, but I can take another pass since TCO seems to be an area of special concern. (In particular, I probably should add some cases exercising more complex chains of inner functions.) |
|
So just as a heads-up: more extensive testing has revealed a number of problems with this patch, which are making me reconsider the approach a little bit. I'll go into more detail when I have more of an opinion formed, but for now, this isn't ready to merge. |
6ac3ffb to
14fe1b7
Compare
|
Okay, I've resolved all the issues I've found. I didn't change anything too dramatically from the previous implementation but I did miss some important details the first time around. Namely: ArityThe initial implementation only considered functions with arity 1. Whoops! Handling functions with higher arity, both as the outermost tail-recursive function and as inner functions participating in the recursion, required a bit more care in the implementation. The stickiest part was what to do with functions that are only partially invoked—in the original TCO, any tail call is necessarily going to have arity matching the function declaration, because it wouldn't type-check otherwise. But allowing other functions to be involved allows for things like: f x y = if y <= 0 then x else g x (y - 1)
where
g x' = f (x' + 2)where even though the recursive call to Nested tail recursionCurrently, functions are TCO'd from the inside out, and they all use the same variable name for loop control. This is not an issue when tail-recursive inner functions don't interact with the tail recursion of outer functions; the inner variable shadows the outer one, which results in correct behavior. Allowing tail recursion to cross function boundaries means that this also needs to be reconsidered; as an example: f x y = g x (y - 1)
where
g x' y' =
if y' <= 0 then x'
else if y' > 10000 then g (x' + 3) (y' - 1)
else f (x' + 2) y'Both |
thomashoneyman
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 This looks great to me!
This commit adds support for optimizing functions that contain local functions which call the outer function in tail position, as long as those functions themselves are only called from tail position, either in the outer function or in other such local functions. This enables hand-written mutually-tail-recursive function groups to be optimized, but more critically, it also means that case guards which desugar to use local functions don't break TCO.
d1f021c to
bd23907
Compare
|
This looks good to me too. @rhendric would you like to hit the 'merge' button on this one? |
|
GitHub still tells me I'm not authorized to do that, but otherwise yes! |
|
haha oops |
|
@rhendric try now? |
This commit adds support for optimizing functions that contain local
functions which call the outer function in tail position, as long as
those functions themselves are only called from tail position, either in
the outer function or in other such local functions.
This enables hand-written mutually-tail-recursive function groups to be
optimized, but more critically, it also means that case guards which
desugar to use local functions don't break TCO.
Closes #3957.