[RFC FS-1011 Discussion] Warn when recursive function is not tail-recursive #82
Replies: 14 comments 17 replies
-
I think it could be useful to get a "warning" of level 4 when recursive functions are not tail-recursive. It's not such a big issue (in normal business code), but it could potentially get you into trouble. However, if you have marked a method as "tailcall" then it sounds like a compiler error? |
Beta Was this translation helpful? Give feedback.
-
That is, perhaps having an information level warning about non tail recursive function in general? |
Beta Was this translation helpful? Give feedback.
-
If this going to be done via an attribute then it absolutely should be a compiler error, not just a warning. I can't see any circumstance where it would be advantageous to tag a function with effectively a tail call suggestion. A compiler error would be useful when working on your own projects because it allows you to impose useful constraints but it becomes, in my opinion, an absolute necessity when library code is considered. Imagine working with a library and seeing If, by contrast, I would also add that I think that adding 'suggestions' that aren't applied at the cost of preventing compilation seems very much at odds with the spirit of type-safe functional programming which is all about compile-time guarantees. |
Beta Was this translation helpful? Give feedback.
-
Yes. That would make more sense @TheInnerLight . It would break existing code though. Could you mark an assembly in some way to get the extra type safety? |
Beta Was this translation helpful? Give feedback.
-
I realized today that the solution I had in mind (which I've detailed a little before, but not well enough) is specifically geared towards warning on non-tail-recursive calls, rather than the more-general case of detecting non-tail-calls. Anyway, here's what I have in mind for the non-tail-recursive call detection: If we can build up a call graph, or rather, a call multigraph, where edges are labeled by some information identifying a specific call site (and nodes are methods/functions), we can use something like Johnson's algorithm to identify all of the simple cycles -- each cycle will represent some chain of recursive calls (which could be as simple as a function calling itself). Any call sites which aren't part of a cycle can immediately be dropped from consideration; they may warrant further analysis to see if they should/could be made tail calls, but for the purposes of detecting recursion they're irrelevant. Any call sites where we've detected a cycle should be tail calls -- if they're not, we'll issue a warning (since there could be unbounded recursion which ends up overflowing the stack). I think this simple analysis could be a useful start to this analyzer, as it would detect the most obviously-wrong cases that may trip up inexperienced F# developers. The analysis could be extended and refined too, by doing some static analysis to detect cycles in the call graph where e.g., some higher-order function accepts a function-typed parameter and calls it; if we consider (for the analysis) functions which are passed into that parameter, we can be pessimistic and warn the user when there's a possibility the call site could lead to unbounded recursion (assuming the higher-order function isn't public; if it is, we have to switch to being optimistic since we can't know everything that could be passed in). Note that the analysis I described doesn't require any additional keywords or attributes, it can be implemented for today's F# code. |
Beta Was this translation helpful? Give feedback.
-
I think in general a way of identifying non-tail calls is interesting, but at best it's a hint to the developer that we may be able to tail call since we don't have access to the information that the Jit has we won't be able to guarantee that the call will treated as tail-recursive by the Jit. What's more, support for tailcall optimizations and the .tail opcodes are not required in a cli compatible runtime project N for example still doesn't support F# style TCO. I am in favor of warning that for let rec foo = .... we are sure the code won't be tailcall optimized, however, a separate keyword seems really iffy given how vague the promise that TCO will happen anyway. I don't have a large care factor whether it is directly in the compiler or as an analyzer. However it should be noted that as an Analyzer we can build much nicer IDE experiences around it, and make enabling it and disabling it much simpler. If it is in the compiler then it will be emitted every let rec that isn't possible to TCO and that will get old fast and likely be an automatic disable on most projects. Warning levels is a possible enabling mechanism in the compiler, however it's not really clear that we have done a good job of being systematic about warning levels. Developers seem to select one of two options "the default --- whatever the system gives you for warning levels if you do nothing" or "the highest --- if they think they gain some benefit from it" Attributes may be interesting if the IDE does a Clippy and says "I notice that you have used
Should it require TCO?, then the IDE can mark it correctly as you type it ... or something Anyway my 2 cents and worth everything you paid .... Kevin |
Beta Was this translation helpful? Give feedback.
-
This is the design thread in fslang-design for [RFC FS-1011 Discussion] Warn when recursive function is not tail-recursive Okay there is a huge and interesting thread over here: dotnet/fsharp#1976 (comment) A little mention for everyone who participated so they get to see this design thread. @AviAvni, @abelbraaksma, @vasily-kirichenko, @forki, @thinkbeforecoding, @vladima, @theimowski, @TeaDrivenDev, @OnurGumus, @OnurGumus, @0x53A, @isaacabraham, @matthid, @ncave, @rojepp, @jack-pappas @dsyme @cartermp Thanks Kevin |
Beta Was this translation helpful? Give feedback.
-
Just to add to the discussion. I feel like when adding this attribute no matter if we emit a warning or an error the compiler should tell me WHY it is not considered tail recursive. For example I've just seen some code here where apparently the code was not tail recursive and after some magic change (removing a parameter) it worked. In other words: If I add the attribute to the linked code-fragment I expect the compiler to tell me why this would not work (might be a good test-case if this was by design) and not just fail with a generic error message (even if that would be already progress). |
Beta Was this translation helpful? Give feedback.
-
I'd like to add that for mutual tail recursion, a simple attribute of type TailRec() = inherit System.Attribute()
let rec [<TailRec>] a x =
if x = 0 then 0
else
printfn "${x}"
a (x-1) // Direct optimisation into a loop
let rec [<TailRec>] b x =
if x < 10 then a x // No tail. instruction here and not a tail call
elif x < 100 then b (x/2) // Direct optimisation into a loop, is a tail call
else c (x/2) // tail. instruction here, is a tail call
and [<TailRec>] c x =
if x < 10 then a x // No tail. instruction here and not a tail call
elif x < 100 then c (x/2) // Direct optimisation into a loop, is a tail call
else b (x/3) // tail. instruction here, is a tail call What will let rec a x =
if x = 0 then 0
else
printfn "${x}"
tailrec a (x-1) // Verified: Direct optimisation into a loop
let rec b x =
if x < 10 then a x
elif x < 100 then tailrec b (x/2) // Verified: Direct optimisation into a loop
else tailrec c (x/2) // Verified: tail. instruction here
and c x =
if x < 10 then a x
elif x < 100 then tailrec c (x/2) // Verified: Direct optimisation into a loop
else tailrec b (x/3) // Verified: tail. instruction here Can we still modify the design to allow call site tagging? |
Beta Was this translation helpful? Give feedback.
-
Revisiting this design, there are two choices to express intent: A. call sites declare "this must be a tailcall" or Do people think (A) or (B) is better? 🚀Pro/cons for A (callsite attribution):
🎉 Pro/cons for B (defintion-site attribute):
Please give comments below and/or react 🚀 for A (attribute callsites with |
Beta Was this translation helpful? Give feedback.
-
@dsyme I'm afraid there's a mixup in the icons. The pro/cons icon usage is different from the last sentence. |
Beta Was this translation helpful? Give feedback.
-
I guess, as some of the stack overflows are prevented by the rewrites done by the optimizer, we need to decide how to handle that, too. From the top of my head, I see two options:
Does this line of thought make sense? |
Beta Was this translation helpful? Give feedback.
-
For people interested in this topic, this might be an interesting issue: |
Beta Was this translation helpful? Give feedback.
-
dotnet/fsharp#15503 goes with a third option. Doing the analysis after the optimization. |
Beta Was this translation helpful? Give feedback.
-
This is the discussion thread for "under review" RFC Warn when recursive function is not tail-recursive
Beta Was this translation helpful? Give feedback.
All reactions